import { Fragment, useEffect, useMemo, 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,
  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 { ServiceAppRole } from "../../entities/access/types";
import type { Client, ClientStatus, ClientTaskManagerWorkspaceBinding, ClientType } from "../../entities/client/types";
import type { Invite, InviteStatus } from "../../entities/invite/types";
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 {
  ClientGroup,
  ClientMembership,
  ClientMembershipRole,
  ClientMembershipStatus,
  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 type { TaskManagerProjectSummary, TaskManagerWorkspaceMemberRole, 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 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 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 rootSections: 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: "groups", label: "Группы", icon: <ListChecks 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 clientSections: Array<{ id: AdminSection; label: string; icon: React.ReactNode }> = [
  { id: "overview", label: "Обзор", icon: <LayoutDashboard size={16} /> },
  { id: "users", label: "Участники", icon: <UsersRound size={16} /> },
  { id: "groups", label: "Группы", icon: <ListChecks size={16} /> },
  { id: "access", label: "Доступы", icon: <KeyRound size={16} /> },
  { id: "invites", label: "Инвайты", icon: <MailPlus size={16} /> },
  { id: "company", label: "Профиль компании", icon: <Building2 size={16} /> },
];

export function AdminOverlay({
  data,
  me,
  activeClientId,
  onClose,
  onSetUserServiceAccess,
  onCreateInvite,
  onUpdateInvite,
  onDeleteInvite,
  onRetrySync,
  onCreateClient,
  onUpdateClient,
  onDeleteClient,
  onCreateUser,
  onUpdateUser,
  onUpdateMembership,
  onDeleteMembership,
  pendingAccessAssignments,
  onCreateGroup,
  onUpdateGroup,
  onDeleteGroup,
  onUpdateService,
  onReorderServices,
  onCreateService,
  onDeleteService,
  onUpdateSettings,
  taskManagerWorkspaces,
  taskManagerWorkspacesLoading,
  taskManagerWorkspacesError,
  pendingTaskManagerMemberships,
  pendingTaskManagerProjectMemberships,
  onRefreshTaskManagerWorkspaces,
  onSetTaskManagerWorkspaceMemberRole,
  onSetTaskManagerProjectMemberRole,
}: {
  data: LauncherData;
  me: MeResponse;
  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;
  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;
  onUpdateMembership: (membershipId: string, patch: Partial<ClientMembership>) => void;
  onDeleteMembership: (membershipId: string) => void;
  pendingAccessAssignments: Record<string, AccessAssignmentValue>;
  onCreateGroup: (clientId: string) => void;
  onUpdateGroup: (groupId: string, patch: Partial<ClientGroup>) => void;
  onDeleteGroup: (groupId: string) => void;
  onUpdateService: (serviceId: string, patch: Partial<Service>) => void;
  onReorderServices: (orderedServiceIds: string[]) => void;
  onCreateService: () => void;
  onDeleteService: (serviceId: string) => void;
  onUpdateSettings: (patch: Partial<LauncherSettings>) => void;
  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;
}) {
  const isRoot = me.launcherRole === "root_admin";
  const sections = isRoot ? rootSections : clientSections;
  const [activeSection, setActiveSection] = useState<AdminSection | null>(null);
  const [isContentFullscreen, setIsContentFullscreen] = useState(false);
  const [selectedClientId, setSelectedClientId] = useState(activeClientId);
  const [selectedCell, setSelectedCell] = useState<{ userId: string; serviceId: string } | null>(null);

  const fallbackClientId = data.clients[0]?.id ?? activeClientId;
  const selectedClientExists = data.clients.some((client) => client.id === selectedClientId);
  const scopedClientId = isRoot ? (selectedClientExists ? selectedClientId : fallbackClientId) : activeClientId;
  const currentClient = getClient(data, scopedClientId);
  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 && !selectedClientExists && data.clients.length) {
      setSelectedClientId(data.clients[0].id);
      setSelectedCell(null);
    }
  }, [data.clients, isRoot, selectedClientExists]);

  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>Администрирование</h2>
          </div>
          <IconButton label="Закрыть администрирование" className="admin-panel-close" onClick={onClose}>
            <X size={15} strokeWidth={1.45} />
          </IconButton>
        </div>

        {isRoot ? (
          <NodeDcSelect
            value={selectedClientId}
            options={data.clients.map((client) => ({ value: client.id, label: client.name, description: client.legalName ?? undefined }))}
            label="Выбрать клиента"
            searchable
            minMenuWidth={292}
            onChange={(clientId) => {
              setSelectedClientId(clientId);
              setSelectedCell(null);
            }}
            trigger={({ open, selectedOption, toggle, setTriggerRef }) => (
              <button
                ref={setTriggerRef}
                className="admin-panel-client-select"
                type="button"
                aria-label="Выбрать клиента"
                aria-expanded={open}
                onClick={toggle}
              >
                <span className="admin-panel-client-select__icon">
                  <Building2 size={16} />
                </span>
                <span className="admin-panel-client-select__name">{selectedOption?.label ?? currentClient.name}</span>
                <span className="admin-panel-client-select__chevron" aria-hidden="true" />
              </button>
            )}
          />
        ) : (
          <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)}
          />
          <div className="admin-panel-content__body">
            {activeSection === "overview" ? <OverviewSection data={data} clientId={scopedClientId} isRoot={isRoot} /> : null}
            {activeSection === "clients" && isRoot ? (
              <ClientsSection
                data={data}
                taskManagerWorkspaces={taskManagerWorkspaces}
                taskManagerWorkspacesLoading={taskManagerWorkspacesLoading}
                taskManagerWorkspacesError={taskManagerWorkspacesError}
                onRefreshTaskManagerWorkspaces={onRefreshTaskManagerWorkspaces}
                onCreateClient={onCreateClient}
                onUpdateClient={onUpdateClient}
                onDeleteClient={onDeleteClient}
              />
            ) : null}
            {activeSection === "users" ? (
              <UsersSection
                data={data}
                clientId={scopedClientId}
                isRoot={isRoot}
                onCreateUser={onCreateUser}
                onUpdateUser={onUpdateUser}
              />
            ) : null}
            {activeSection === "groups" ? (
              <GroupsSection
                data={data}
                clientId={scopedClientId}
                onCreateGroup={onCreateGroup}
                onUpdateGroup={onUpdateGroup}
                onDeleteGroup={onDeleteGroup}
              />
            ) : null}
            {activeSection === "services" && isRoot ? (
              <ServicesSection
                data={data}
                onUpdateService={onUpdateService}
                onReorderServices={onReorderServices}
                onCreateService={onCreateService}
                onDeleteService={onDeleteService}
              />
            ) : null}
            {activeSection === "access" ? (
              <AccessSection
                data={data}
                matrix={accessMatrix}
                selectedCell={selectedAccessCell}
                onSelectCell={(cell) => setSelectedCell({ userId: cell.userId, serviceId: cell.serviceId })}
                onSetUserServiceAccess={onSetUserServiceAccess}
                pendingAccessAssignments={pendingAccessAssignments}
                onUpdateUser={onUpdateUser}
                onUpdateMembership={onUpdateMembership}
                pendingTaskManagerMemberships={pendingTaskManagerMemberships}
                pendingTaskManagerProjectMemberships={pendingTaskManagerProjectMemberships}
                taskManagerWorkspaceCatalog={taskManagerWorkspaces}
                onSetTaskManagerWorkspaceMemberRole={onSetTaskManagerWorkspaceMemberRole}
                onSetTaskManagerProjectMemberRole={onSetTaskManagerProjectMemberRole}
              />
            ) : null}
            {activeSection === "invites" ? (
              <InvitesSection
                data={data}
                clientId={scopedClientId}
                actorUserId={me.user.id}
                onCreateInvite={onCreateInvite}
                onUpdateInvite={onUpdateInvite}
                onDeleteInvite={onDeleteInvite}
              />
            ) : null}
            {activeSection === "sync" ? <SyncSection data={data} clientId={scopedClientId} isRoot={isRoot} onRetrySync={onRetrySync} /> : null}
            {activeSection === "audit" && isRoot ? <AuditSection data={data} /> : null}
            {activeSection === "misc" && isRoot ? <MiscSection data={data} onUpdateSettings={onUpdateSettings} /> : null}
            {activeSection === "company" ? <CompanySection data={data} clientId={scopedClientId} /> : null}
          </div>
        </section>
      ) : null}
    </div>
  );
}

function AdminHeader({
  isFullscreen,
  onToggleFullscreen,
  onCloseContent,
}: {
  isFullscreen: boolean;
  onToggleFullscreen: () => void;
  onCloseContent: () => void;
}) {
  return (
    <div className="admin-header">
      <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 OverviewSection({ data, clientId, isRoot }: { data: LauncherData; clientId: string; isRoot: boolean }) {
  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" && (isRoot || sync.objectId === clientId)).length;

  return (
    <section className="admin-section-grid">
      <MetricCard label={isRoot ? "Клиентов" : "Участников"} value={isRoot ? data.clients.length : 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">
        <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,
  isRoot,
  onCreateUser,
  onUpdateUser,
}: {
  data: LauncherData;
  clientId: string;
  isRoot: boolean;
  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 = isRoot
    ? data.memberships.map((membership) => ({ membership, user: getUser(data, membership.userId), client: getClient(data, membership.clientId) }))
    : 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>
            </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>
                    <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 GroupsSection({
  data,
  clientId,
  onCreateGroup,
  onUpdateGroup,
  onDeleteGroup,
}: {
  data: LauncherData;
  clientId: string;
  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}
          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 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 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 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 operationalCoreRoleOptions: Array<NodeDcSelectOption<OperationalCoreRoleSelectValue>> = [
  ...accessAssignmentOptions,
  { 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" },
  { value: "pending", label: "Сохраняем...", disabled: true, hidden: true },
];

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

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,
    });
  }

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

  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 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,
    });
  }

  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;

function ServicesSection({
  data,
  onUpdateService,
  onReorderServices,
  onCreateService,
  onDeleteService,
}: {
  data: LauncherData;
  onUpdateService: (serviceId: string, patch: Partial<Service>) => void;
  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">
          <h3>Каталог сервисов</h3>
          <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={(patch) => {
            onUpdateService(contentService.id, patch);
            setContentServiceId(null);
          }}
          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>) => void;
  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>) => void;
  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>) => void;
  onDelete: () => void;
}) {
  const [draft, setDraft] = useState<Service>(service);
  const [uploadingSlot, setUploadingSlot] = useState<"cover" | "ambient" | null>(null);
  const [storageError, setStorageError] = useState<string | null>(null);
  const [deleteOpen, setDeleteOpen] = useState(false);

  useEffect(() => {
    setDraft(service);
    setStorageError(null);
    setUploadingSlot(null);
  }, [service]);

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

  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") {
    setStorageError(null);
    setUploadingSlot(slot);

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

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

  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) => setDraft((current) => ({ ...current, ...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"}
            onSourceChange={(source) => update("coverMediaSource", source)}
            onUrlChange={(value) => {
              update("coverImageUrl", value || null);
              update("coverMediaSource", "url");
              update("coverMediaKind", mediaKindFromUrl(value));
              update("coverMediaFileName", null);
            }}
            onFileChange={handleCoverUpload}
          />

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

          <div className="service-content-preview service-content-preview--image">
            {draft.coverImageUrl ? <MediaPreview src={draft.coverImageUrl} kind={draft.coverMediaKind} /> : <ImageIcon size={30} />}
          </div>

          <div className="service-content-preview service-content-preview--video">
            {draft.ambientVideoUrl ? <MediaPreview src={draft.ambientVideoUrl} kind={draft.ambientMediaKind} /> : <Video size={30} />}
          </div>

          {storageError ? <p className="service-content-storage-error">{storageError}</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}
              icon={<Save size={16} />}
              onClick={() =>
                onSave({
                  title: draft.title,
                  subtitle: draft.subtitle,
                  description: draft.description,
                  fullDescription: draft.fullDescription,
                  url: draft.url,
                  launchUrl: draft.launchUrl,
                  coverImageUrl: draft.coverImageUrl,
                  coverMediaKind: draft.coverMediaKind,
                  coverMediaSource: draft.coverMediaSource,
                  coverMediaFileName: draft.coverMediaFileName,
                  ambientVideoUrl: draft.ambientVideoUrl,
                  ambientMediaKind: draft.ambientMediaKind,
                  ambientMediaSource: draft.ambientMediaSource,
                  ambientMediaFileName: draft.ambientMediaFileName,
                })
            }
            >
              {uploadingSlot ? "Сохраняем файл" : "Сохранить"}
            </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;
}) {
  const [draft, setDraft] = useState<Client>(client);
  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);
    setUploadingAvatar(false);
    setStorageError(null);
  }, [client]);

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

  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 = exists
      ? currentWorkspaces.filter((item) => item.slug !== workspace.slug)
      : [
          ...currentWorkspaces,
          {
            slug: workspace.slug,
            name: workspace.name,
            isPrimary: currentWorkspaces.length === 0,
          },
        ];

    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;

    setUploadingAvatar(true);
    setStorageError(null);

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

  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>
          }
        />
        <div className="service-content-modal__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">
                {draft.avatarUrl ? <img src={draft.avatarUrl} alt="" /> : null}
              </div>
              <div className="client-avatar-control__copy">
                <strong>{draft.avatarUrl ? "Аватар подключён" : "Аватар не задан"}</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>
              {draft.avatarUrl ? (
                <button className="admin-icon-action client-avatar-clear-action" type="button" onClick={() => update("avatarUrl", 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} участников
                          </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={onClose}
          onSave={() => onSave(draft)}
          deleteConfig={
            canDelete
              ? {
                  label: "Удалить",
                  title: "Удалить компанию",
                  description: (
                    <>
                      Компания <strong>{client.name}</strong>, ее участники, группы, инвайты и клиентские гранты будут удалены из демо-данных.
                    </>
                  ),
                  onConfirm: onDelete,
                }
              : undefined
          }
        />
      </article>
    </div>
  );
}

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,
  users,
  onClose,
  onSave,
  onDelete,
}: {
  group: ClientGroup;
  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) => (
                <button
                  key={user.id}
                  className="admin-token"
                  data-active={draft.memberIds.includes(user.id)}
                  type="button"
                  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?: {
    label: string;
    title: string;
    description: ReactNode;
    onConfirm: () => void;
  };
}) {
  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,
  onSourceChange,
  onUrlChange,
  onFileChange,
}: {
  label: string;
  icon: ReactNode;
  source: ServiceMediaSource;
  value: string;
  fileName?: string | null;
  isUploading?: boolean;
  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-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>
    </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,
  matrix,
  selectedCell,
  onSelectCell,
  onSetUserServiceAccess,
  pendingAccessAssignments,
  onUpdateUser,
  onUpdateMembership,
  pendingTaskManagerMemberships,
  pendingTaskManagerProjectMemberships,
  taskManagerWorkspaceCatalog,
  onSetTaskManagerWorkspaceMemberRole,
  onSetTaskManagerProjectMemberRole,
}: {
  data: LauncherData;
  matrix: ReturnType<typeof buildAccessMatrix>;
  selectedCell: AccessMatrixCell | null;
  onSelectCell: (cell: AccessMatrixCell) => void;
  onSetUserServiceAccess: (command: SetUserServiceAccessCommand) => void;
  pendingAccessAssignments: Record<string, AccessAssignmentValue>;
  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;
}) {
  const hasUsers = matrix.users.length > 0;
  const clientTaskManagerWorkspaces = getClientTaskManagerWorkspaces(matrix.client);
  const primaryTaskManagerWorkspace = getPrimaryTaskManagerWorkspace(matrix.client);
  const [detailsCell, setDetailsCell] = useState<AccessMatrixCell | null>(null);

  if (!hasUsers) {
    return (
      <div className="access-layout">
        <GlassSurface className="access-matrix">
          <div className="table-toolbar">
            <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) repeat(${matrix.services.length}, 11.25rem)`;

  return (
    <div className="access-layout">
      <GlassSurface className="access-matrix">
        <div className="table-toolbar">
          <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>
            {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 = user.id === "user_root";
              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>
                  </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}
                      onChange={(role) => onUpdateMembership(membership.id, { role })}
                    />
                  </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);

                    return (
                      <div key={service.id} className="access-grid-cell" role="cell">
                        <AccessCellControl
                          cell={cell}
                          active={active}
                          pendingValue={pendingAccessAssignments[accessCellKey(user.id, service.id)]}
                          busy={isTaskManagerService && pendingTaskerAssignment}
                          onSelectCell={onSelectCell}
                          onSetAccess={(value) => {
                            onSetUserServiceAccess({ userId: user.id, serviceId: service.id, value });

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

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

      {detailsCell ? (
        <OperationalCoreAccessModal
          data={data}
          client={matrix.client}
          user={getUser(data, detailsCell.userId)}
          service={getService(data, detailsCell.serviceId)}
          cell={detailsCell}
          workspaces={clientTaskManagerWorkspaces}
          workspaceCatalog={taskManagerWorkspaceCatalog}
          pendingAccessAssignments={pendingAccessAssignments}
          pendingTaskManagerMemberships={pendingTaskManagerMemberships}
          pendingTaskManagerProjectMemberships={pendingTaskManagerProjectMemberships}
          onClose={() => setDetailsCell(null)}
          onSetUserServiceAccess={onSetUserServiceAccess}
          onSetTaskManagerWorkspaceMemberRole={onSetTaskManagerWorkspaceMemberRole}
          onSetTaskManagerProjectMemberRole={onSetTaskManagerProjectMemberRole}
        />
      ) : null}
    </div>
  );
}

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={172}
      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 OperationalCoreAccessModal({
  data,
  client,
  user,
  service,
  cell,
  workspaces,
  workspaceCatalog,
  pendingAccessAssignments,
  pendingTaskManagerMemberships,
  pendingTaskManagerProjectMemberships,
  onClose,
  onSetUserServiceAccess,
  onSetTaskManagerWorkspaceMemberRole,
  onSetTaskManagerProjectMemberRole,
}: {
  data: LauncherData;
  client: Client;
  user: LauncherUser;
  service: Service;
  cell: AccessMatrixCell;
  workspaces: ClientTaskManagerWorkspaceBinding[];
  workspaceCatalog: TaskManagerWorkspaceSummary[];
  pendingAccessAssignments: Record<string, AccessAssignmentValue>;
  pendingTaskManagerMemberships: Record<string, boolean>;
  pendingTaskManagerProjectMemberships: Record<string, boolean>;
  onClose: () => void;
  onSetUserServiceAccess: (command: SetUserServiceAccessCommand) => void;
  onSetTaskManagerWorkspaceMemberRole: (command: EnsureTaskManagerWorkspaceMemberCommand) => void;
  onSetTaskManagerProjectMemberRole: (command: EnsureTaskManagerProjectMemberCommand) => void;
}) {
  const membership = data.memberships.find((item) => item.clientId === client.id && item.userId === user.id);
  const protectedUser = user.id === "user_root" || membership?.role === "client_owner";
  const basePendingValue = pendingAccessAssignments[accessCellKey(user.id, service.id)];
  const baseAssignmentValue = basePendingValue ?? accessAssignmentValue(cell);

  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">
          {workspaces.length ? (
            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 ? (
                      <AdminStaticPill>{accessAssignmentLabel("admin")}</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;
                          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 ? (
                            <AdminStaticPill>{accessAssignmentLabel("admin")}</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;

                                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>
              );
            })
          ) : (
            <div className="access-empty-state">
              <strong>Workspace не привязаны к клиенту</strong>
              <span>Откройте карточку клиента и выберите Operational Core workspaces, после этого здесь появятся назначения.</span>
            </div>
          )}
        </div>

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

function AccessCellControl({
  cell,
  active,
  pendingValue,
  busy = false,
  onSelectCell,
  onSetAccess,
  onOpenDetails,
}: {
  cell: AccessMatrixCell;
  active: boolean;
  pendingValue?: AccessAssignmentValue;
  busy?: boolean;
  onSelectCell: (cell: AccessMatrixCell) => void;
  onSetAccess: (value: AccessAssignmentValue) => void;
  onOpenDetails?: () => void;
}) {
  const isPending = pendingValue !== undefined || busy;
  const assignmentValue = pendingValue ?? accessAssignmentValue(cell);
  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" && "access-cell--exception",
    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>{isPending ? accessAssignmentLabel(assignmentValue) : accessCellTitle(cell)}</strong>
        <span>{isPending ? "Сохраняем..." : sourceLabel(cell.effectiveAccess.source)}</span>
      </button>
    );
  }

  return (
    <NodeDcSelect
      value={assignmentValue}
      options={accessAssignmentOptions}
      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>{isPending ? accessAssignmentLabel(assignmentValue) : accessCellTitle(cell)}</strong>
          <span>{isPending ? "Сохраняем..." : sourceLabel(cell.effectiveAccess.source)}</span>
        </button>
      )}
    />
  );
}

function InvitesSection({
  data,
  clientId,
  actorUserId,
  onCreateInvite,
  onUpdateInvite,
  onDeleteInvite,
}: {
  data: LauncherData;
  clientId: string;
  actorUserId: string;
  onCreateInvite: (invite: Pick<Invite, "clientId" | "email" | "role">) => void;
  onUpdateInvite: (inviteId: string, patch: Partial<Invite>) => void;
  onDeleteInvite: (inviteId: string) => 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 deletingInvite = invites.find((invite) => invite.id === deleteInviteId) ?? null;
  const actor = getUser(data, actorUserId);

  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">
      <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">
          <h3>Инвайты</h3>
        </div>
        <table className="admin-data-table">
          <thead>
            <tr>
              <th>Email</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;

              return (
                <tr key={invite.id}>
                  <td>
                    <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>
                    <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>
                    <AdminStatusDropdown
                      value={invite.status}
                      options={inviteStatusOptions}
                      label={`Статус инвайта ${invite.email}`}
                      onChange={(status) => onUpdateInvite(invite.id, { status })}
                    />
                  </td>
                  <td>
                    <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>
                  </td>
                  <td>
                    <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={
          <>
            Инвайт для <strong>{deletingInvite?.email}</strong> будет удален вместе с токеном приглашения.
          </>
        }
        onClose={() => setDeleteInviteId(null)}
        onConfirm={() => {
          if (deletingInvite) onDeleteInvite(deletingInvite.id);
          setDeleteInviteId(null);
        }}
      />
    </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,
  onRetrySync,
}: {
  data: LauncherData;
  clientId: string;
  isRoot: boolean;
  onRetrySync: (syncId: string) => void;
}) {
  const syncRows = data.syncStatuses.filter((sync) => isRoot || sync.objectId === clientId || sync.objectName.includes(getClient(data, clientId).name));

  return (
    <GlassSurface className="table-shell">
      <div className="table-toolbar">
        <div>
          <h3>Синхронизация</h3>
          <p className="admin-helper-note">Контроль доставки изменений в Authentik, сервисы и внешние контуры. Ошибочные строки можно отправить повторно.</p>
        </div>
      </div>
      <table className="admin-data-table">
        <thead>
          <tr>
            <th>Объект</th>
            <th>Тип</th>
            <th>Цель</th>
            <th>Статус</th>
            <th>Последняя sync</th>
            <th>Ошибка</th>
            <th>Действие</th>
          </tr>
        </thead>
        <tbody>
          {syncRows.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>
    </GlassSurface>
  );
}

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>) => void;
}) {
  const [logoLinkUrl, setLogoLinkUrl] = useState(data.settings.brand.logoLinkUrl);
  const [workspaceCreationPolicy, setWorkspaceCreationPolicy] = useState(
    data.settings.taskManager.workspaceCreationPolicy
  );

  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;

  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}
          onClick={() =>
            onUpdateSettings({
              brand: { logoLinkUrl: normalizedLogoLinkUrl },
              taskManager: { workspaceCreationPolicy },
            })
          }
        >
          Сохранить
        </Button>
      </div>

      <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)}
          />
          <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)}
          />
          <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 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 accessCellKey(userId: string, serviceId: string): string {
  return `${userId}:${serviceId}`;
}

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 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];
}
