import { randomUUID } from "node:crypto";
import { existsSync, readFileSync } from "node:fs";
import { mkdir, rename, writeFile } from "node:fs/promises";
import { join } from "node:path";

const collectionKeys = [
  "clients",
  "users",
  "memberships",
  "groups",
  "services",
  "grants",
  "exceptions",
  "invites",
  "syncStatuses",
  "auditEvents",
  "taskManagerMemberships",
  "taskManagerProjectMemberships",
];

const clientTypes = new Set(["company", "person"]);
const clientStatuses = new Set(["active", "suspended", "demo", "expired"]);
const userStatuses = new Set(["invited", "active", "blocked"]);
const membershipRoles = new Set(["client_owner", "client_admin", "member"]);
const grantTargetTypes = new Set(["client", "group", "user"]);
const appRoles = new Set(["viewer", "member", "admin", "owner"]);
const grantStatuses = new Set(["active", "disabled"]);
const exceptionTypes = new Set(["deny", "allow"]);
const serviceStatuses = new Set(["active", "maintenance", "hidden", "disabled"]);
const defaultSettings = {
  brand: {
    logoLinkUrl: "/",
  },
  taskManager: {
    workspaceCreationPolicy: "any_authorized_user",
  },
};

export function createControlPlaneStore({ projectRoot }) {
  const publicStorageRoot = join(projectRoot, "public", "storage");
  const distStorageRoot = join(projectRoot, "dist", "storage");
  const dataPath = join(publicStorageRoot, "launcher-data.json");

  function readData() {
    if (!existsSync(dataPath)) {
      return normalizeData({});
    }

    return normalizeData(JSON.parse(readFileSync(dataPath, "utf8")));
  }

  async function writeData(data) {
    const normalizedData = normalizeData(data);
    const payload = `${JSON.stringify(normalizedData, null, 2)}\n`;

    await Promise.all(
      getWritableStorageRoots().map(async (storageRoot) => {
        await mkdir(storageRoot, { recursive: true });
        await writeJsonAtomically(join(storageRoot, "launcher-data.json"), payload);
      })
    );

    return normalizedData;
  }

  function getSnapshot(identity) {
    const data = readData();

    return {
      actor: resolveActor(data, identity),
      counts: Object.fromEntries(collectionKeys.map((key) => [key, data[key].length])),
      data,
    };
  }

  async function replaceData(payload, identity) {
    const data = normalizeData(payload);
    const actor = resolveActor(data, identity);
    addAuditEvent(data, actor, {
      action: "Обновлено control-plane состояние",
      objectType: "control_plane",
      objectName: "Launcher data",
      result: "success",
      details: "Полная запись launcher-data.json через backend store.",
    });

    return writeData(data);
  }

  async function createClient(payload, identity) {
    const data = readData();
    const actor = resolveActor(data, identity);
    const now = isoNow();
    const name = requireString(payload?.name, "name");
    const client = {
      id: uniqueId(data.clients, "client", name),
      type: pickEnum(payload?.type, clientTypes, "company"),
      name,
      legalName: nullableString(payload?.legalName),
      inn: nullableString(payload?.inn),
      status: pickEnum(payload?.status, clientStatuses, "active"),
      contractStartsAt: nullableString(payload?.contractStartsAt),
      contractEndsAt: nullableString(payload?.contractEndsAt),
      paidUntil: nullableString(payload?.paidUntil),
      demoEndsAt: nullableString(payload?.demoEndsAt),
      contactName: nullableString(payload?.contactName),
      contactEmail: nullableString(payload?.contactEmail),
      integrations: normalizeClientIntegrations(payload?.integrations),
      notes: nullableString(payload?.notes),
      createdAt: now,
      updatedAt: now,
    };

    data.clients.push(client);
    addAuditEvent(data, actor, {
      action: "Создан клиент",
      objectType: "client",
      objectName: client.name,
      clientId: client.id,
      result: "success",
    });
    markPendingSync(data, client, "client");

    await writeData(data);
    return { client, data };
  }

  async function updateClient(clientId, payload, identity) {
    const data = readData();
    const actor = resolveActor(data, identity);
    const client = findById(data.clients, clientId, "client");

    client.type = pickEnum(payload?.type, clientTypes, client.type);
    client.name = optionalString(payload?.name, client.name);
    client.legalName = nullableStringWithFallback(payload?.legalName, client.legalName ?? null);
    client.inn = nullableStringWithFallback(payload?.inn, client.inn ?? null);
    client.status = pickEnum(payload?.status, clientStatuses, client.status);
    client.contractStartsAt = nullableStringWithFallback(payload?.contractStartsAt, client.contractStartsAt ?? null);
    client.contractEndsAt = nullableStringWithFallback(payload?.contractEndsAt, client.contractEndsAt ?? null);
    client.paidUntil = nullableStringWithFallback(payload?.paidUntil, client.paidUntil ?? null);
    client.demoEndsAt = nullableStringWithFallback(payload?.demoEndsAt, client.demoEndsAt ?? null);
    client.contactName = nullableStringWithFallback(payload?.contactName, client.contactName ?? null);
    client.contactEmail = nullableStringWithFallback(payload?.contactEmail, client.contactEmail ?? null);
    if ("integrations" in (payload ?? {})) {
      client.integrations = normalizeClientIntegrations(payload.integrations, client.integrations);
    }
    client.notes = nullableStringWithFallback(payload?.notes, client.notes ?? null);
    client.updatedAt = isoNow();

    addAuditEvent(data, actor, {
      action: "Обновлён клиент",
      objectType: "client",
      objectName: client.name,
      clientId: client.id,
      result: "success",
    });
    markPendingSync(data, client, "client");

    await writeData(data);
    return { client, data };
  }

  async function recordTaskManagerWorkspaceMembership(payload, identity) {
    const data = readData();
    const actor = resolveActor(data, identity);
    const client = findById(data.clients, payload?.clientId, "client");
    const user = findById(data.users, payload?.userId, "user");
    const taskManager = typeof payload?.taskManager === "object" && payload.taskManager !== null ? payload.taskManager : {};
    const membership = typeof taskManager.membership === "object" && taskManager.membership !== null ? taskManager.membership : {};
    const workspace = typeof membership.workspace === "object" && membership.workspace !== null ? membership.workspace : {};

    upsertTaskManagerMembership(data, {
      clientId: client.id,
      userId: user.id,
      workspaceSlug: workspace.slug ?? payload?.workspaceSlug,
      workspaceName: workspace.name ?? payload?.workspaceName ?? null,
      role: normalizeTaskManagerMembershipRole(payload?.role),
      planeUserId: membership.member?.id ?? null,
      planeRole: typeof membership.role === "number" ? membership.role : null,
    });

    addAuditEvent(data, actor, {
      action: "Назначен Tasker workspace",
      objectType: "task-manager-membership",
      objectName: user.name,
      clientId: client.id,
      result: "success",
      details: `Workspace: ${workspace.name ?? workspace.slug ?? payload?.workspaceSlug}; Role: ${payload?.role ?? "member"}`,
    });

    await writeData(data);
    return { data };
  }

  async function removeTaskManagerWorkspaceMembership(payload, identity) {
    const data = readData();
    const actor = resolveActor(data, identity);
    const client = findById(data.clients, payload?.clientId, "client");
    const user = findById(data.users, payload?.userId, "user");
    const workspaceSlug = requireString(payload?.workspaceSlug ?? client.integrations?.taskManager?.workspaceSlug, "workspaceSlug");

    data.taskManagerMemberships = data.taskManagerMemberships.filter(
      (membership) => !(membership.clientId === client.id && membership.userId === user.id && membership.workspaceSlug === workspaceSlug)
    );
    data.taskManagerProjectMemberships = data.taskManagerProjectMemberships.filter(
      (membership) => !(membership.clientId === client.id && membership.userId === user.id && membership.workspaceSlug === workspaceSlug)
    );

    addAuditEvent(data, actor, {
      action: "Снят Tasker workspace",
      objectType: "task-manager-membership",
      objectName: user.name,
      clientId: client.id,
      result: "success",
      details: `Workspace: ${workspaceSlug}`,
    });

    await writeData(data);
    return { data };
  }

  async function recordTaskManagerProjectMembership(payload, identity) {
    const data = readData();
    const actor = resolveActor(data, identity);
    const client = findById(data.clients, payload?.clientId, "client");
    const user = findById(data.users, payload?.userId, "user");
    const taskManager = typeof payload?.taskManager === "object" && payload.taskManager !== null ? payload.taskManager : {};
    const membership = typeof taskManager.membership === "object" && taskManager.membership !== null ? taskManager.membership : {};
    const workspace = typeof membership.workspace === "object" && membership.workspace !== null ? membership.workspace : {};
    const project = typeof membership.project === "object" && membership.project !== null ? membership.project : {};

    upsertTaskManagerProjectMembership(data, {
      clientId: client.id,
      userId: user.id,
      workspaceSlug: workspace.slug ?? payload?.workspaceSlug,
      workspaceName: workspace.name ?? payload?.workspaceName ?? null,
      projectId: project.id ?? payload?.projectId,
      projectIdentifier: project.identifier ?? payload?.projectIdentifier ?? null,
      projectName: project.name ?? payload?.projectName ?? null,
      role: normalizeTaskManagerMembershipRole(payload?.role),
      planeUserId: membership.member?.id ?? null,
      planeRole: typeof membership.role === "number" ? membership.role : null,
    });

    addAuditEvent(data, actor, {
      action: "Назначен Tasker project",
      objectType: "task-manager-project-membership",
      objectName: user.name,
      clientId: client.id,
      result: "success",
      details: `Workspace: ${workspace.name ?? workspace.slug ?? payload?.workspaceSlug}; Project: ${project.name ?? project.identifier ?? payload?.projectId}; Role: ${payload?.role ?? "member"}`,
    });

    await writeData(data);
    return { data };
  }

  async function removeTaskManagerProjectMembership(payload, identity) {
    const data = readData();
    const actor = resolveActor(data, identity);
    const client = findById(data.clients, payload?.clientId, "client");
    const user = findById(data.users, payload?.userId, "user");
    const workspaceSlug = requireString(payload?.workspaceSlug ?? client.integrations?.taskManager?.workspaceSlug, "workspaceSlug");
    const projectId = requireString(payload?.projectId, "projectId");

    data.taskManagerProjectMemberships = data.taskManagerProjectMemberships.filter(
      (membership) =>
        !(
          membership.clientId === client.id &&
          membership.userId === user.id &&
          membership.workspaceSlug === workspaceSlug &&
          membership.projectId === projectId
        )
    );

    addAuditEvent(data, actor, {
      action: "Снят Tasker project",
      objectType: "task-manager-project-membership",
      objectName: user.name,
      clientId: client.id,
      result: "success",
      details: `Workspace: ${workspaceSlug}; Project: ${projectId}`,
    });

    await writeData(data);
    return { data };
  }

  async function deleteClient(clientId, identity) {
    const data = readData();
    const actor = resolveActor(data, identity);
    const client = findById(data.clients, clientId, "client");

    if (data.clients.length <= 1) {
      throw new Error("Cannot delete the last client");
    }

    const deletedGroupIds = new Set(data.groups.filter((group) => group.clientId === clientId).map((group) => group.id));
    data.clients = data.clients.filter((item) => item.id !== clientId);
    data.memberships = data.memberships.filter((membership) => membership.clientId !== clientId);
    data.groups = data.groups.filter((group) => group.clientId !== clientId);
    data.grants = data.grants.filter(
      (grant) =>
        !(grant.targetType === "client" && grant.targetId === clientId) &&
        !(grant.targetType === "group" && deletedGroupIds.has(grant.targetId))
    );
    data.invites = data.invites.filter((invite) => invite.clientId !== clientId);
    data.syncStatuses = data.syncStatuses.filter((syncStatus) => syncStatus.objectId !== clientId);

    addAuditEvent(data, actor, {
      action: "Удалён клиент",
      objectType: "client",
      objectName: client.name,
      clientId,
      result: "warning",
    });

    await writeData(data);
    return { client, data };
  }

  async function updateSettings(payload, identity) {
    const data = readData();
    const actor = resolveActor(data, identity);
    const patch = typeof payload === "object" && payload !== null ? payload : {};
    const settings = normalizeSettings({
      ...data.settings,
      ...patch,
      brand: {
        ...(data.settings?.brand ?? {}),
        ...(patch.brand ?? {}),
      },
      taskManager: {
        ...(data.settings?.taskManager ?? {}),
        ...(patch.taskManager ?? {}),
      },
    });

    data.settings = settings;
    addAuditEvent(data, actor, {
      action: "Обновлены системные настройки",
      objectType: "settings",
      objectName: "Brand settings",
      result: "success",
      details: `Logo link: ${settings.brand.logoLinkUrl}; Tasker workspace policy: ${settings.taskManager.workspaceCreationPolicy}`,
    });

    await writeData(data);
    return { settings, data };
  }

  async function updateUserProfile(userId, payload, identity) {
    const data = readData();
    const actor = resolveActor(data, identity);
    const user = findById(data.users, userId, "user");
    const now = isoNow();

    user.name = optionalString(payload?.name, user.name);
    user.email = optionalString(payload?.email, user.email);
    user.phone = nullableStringWithFallback(payload?.phone, user.phone);
    user.position = nullableStringWithFallback(payload?.position, user.position);
    user.notes = nullableStringWithFallback(payload?.notes, user.notes);
    user.avatarUrl = nullableStringWithFallback(payload?.avatarUrl, user.avatarUrl ?? null);
    user.globalStatus = pickEnum(payload?.globalStatus, userStatuses, user.globalStatus);
    user.updatedAt = now;

    addAuditEvent(data, actor, {
      action: "Обновлён профиль пользователя",
      objectType: "user",
      objectName: user.email,
      result: "success",
    });
    markPendingSync(data, user, "user");

    await writeData(data);
    return { user, data };
  }

  async function createUser(payload, identity) {
    const data = readData();
    const actor = resolveActor(data, identity);
    const now = isoNow();
    const clientId = requireString(payload?.clientId, "clientId");
    const client = findById(data.clients, clientId, "client");
    const email = requireString(payload?.email, "email").toLowerCase();
    const existingUser = data.users.find((item) => item.email.toLowerCase() === email);
    const user =
      existingUser ??
      {
        id: uniqueId(data.users, "user", email),
        authentikUserId: nullableString(payload?.authentikUserId),
        name: optionalString(payload?.name, email.split("@")[0]),
        email,
        phone: nullableString(payload?.phone),
        position: nullableString(payload?.position),
        notes: nullableString(payload?.notes),
        avatarUrl: nullableString(payload?.avatarUrl),
        globalStatus: pickEnum(payload?.globalStatus, userStatuses, "active"),
        createdAt: now,
        updatedAt: now,
      };

    if (existingUser) {
      user.name = optionalString(payload?.name, user.name);
      user.phone = nullableStringWithFallback(payload?.phone, user.phone ?? null);
      user.position = nullableStringWithFallback(payload?.position, user.position ?? null);
      user.notes = nullableStringWithFallback(payload?.notes, user.notes ?? null);
      user.avatarUrl = nullableStringWithFallback(payload?.avatarUrl, user.avatarUrl ?? null);
      user.globalStatus = pickEnum(payload?.globalStatus, userStatuses, user.globalStatus);
      user.updatedAt = now;
    } else {
      data.users.push(user);
    }

    const existingMembership = data.memberships.find((membership) => membership.clientId === clientId && membership.userId === user.id);

    if (existingMembership) {
      throw new Error(`User ${email} already belongs to client ${client.name}`);
    }

    const membership = {
      id: uniqueId(data.memberships, "mem", `${clientId}-${email}`),
      clientId,
      userId: user.id,
      role: pickEnum(payload?.role, membershipRoles, "member"),
      status: pickEnum(payload?.membershipStatus, new Set(["active", "disabled"]), "active"),
      createdAt: now,
      updatedAt: now,
    };

    data.memberships.push(membership);

    if (Array.isArray(payload?.groupIds)) {
      for (const group of data.groups) {
        if (group.clientId !== clientId || !payload.groupIds.includes(group.id) || group.memberIds.includes(user.id)) {
          continue;
        }

        group.memberIds.push(user.id);
        group.updatedAt = now;
      }
    }

    addAuditEvent(data, actor, {
      action: existingUser ? "Добавлен пользователь в клиента" : "Создан пользователь",
      objectType: "user",
      objectName: email,
      clientId: client.id,
      result: "success",
      details: `Role: ${membership.role}; status: ${membership.status}`,
    });
    markPendingSync(data, user, "user", email);

    await writeData(data);
    return { user, membership, data };
  }

  async function updateMembership(membershipId, payload, identity) {
    const data = readData();
    const actor = resolveActor(data, identity);
    const membership = findById(data.memberships, membershipId, "membership");
    const user = findById(data.users, membership.userId, "user");

    membership.role = pickEnum(payload?.role, membershipRoles, membership.role);
    membership.status = pickEnum(payload?.status, new Set(["active", "disabled"]), membership.status);
    membership.updatedAt = isoNow();

    addAuditEvent(data, actor, {
      action: "Обновлено членство",
      objectType: "user",
      objectName: user.email,
      clientId: membership.clientId,
      result: "success",
      details: `Role: ${membership.role}; status: ${membership.status}`,
    });
    markPendingSync(data, user, "user");

    await writeData(data);
    return { membership, data };
  }

  async function deleteMembership(membershipId, identity) {
    const data = readData();
    const actor = resolveActor(data, identity);
    const membership = findById(data.memberships, membershipId, "membership");
    const user = findById(data.users, membership.userId, "user");

    data.memberships = data.memberships.filter((item) => item.id !== membershipId);
    data.groups = data.groups.map((group) =>
      group.clientId === membership.clientId
        ? {
            ...group,
            memberIds: group.memberIds.filter((userId) => userId !== membership.userId),
            updatedAt: isoNow(),
          }
        : group
    );

    addAuditEvent(data, actor, {
      action: "Удалено членство",
      objectType: "user",
      objectName: user.email,
      clientId: membership.clientId,
      result: "warning",
    });
    markPendingSync(data, user, "user");

    await writeData(data);
    return { membership, data };
  }

  async function createInvite(payload, identity) {
    const data = readData();
    const actor = resolveActor(data, identity);
    const now = isoNow();
    const clientId = requireString(payload?.clientId, "clientId");
    const client = findById(data.clients, clientId, "client");
    const email = requireString(payload?.email, "email").toLowerCase();
    const role = pickEnum(payload?.role, membershipRoles, "member");
    const expiresAt = optionalString(payload?.expiresAt, new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString());
    const invite = {
      id: uniqueId(data.invites, "invite", email),
      clientId,
      email,
      role,
      invitedByUserId: actor.id,
      token: randomUUID(),
      expiresAt,
      status: "created",
      createdAt: now,
      updatedAt: now,
    };

    data.invites.push(invite);
    addAuditEvent(data, actor, {
      action: "Создан инвайт",
      objectType: "invite",
      objectName: email,
      clientId: client.id,
      result: "success",
      details: `Роль: ${role}`,
    });
    markPendingSync(data, invite, "invite", email);

    await writeData(data);
    return { invite, data };
  }

  async function updateInvite(inviteId, payload, identity) {
    const data = readData();
    const actor = resolveActor(data, identity);
    const invite = findById(data.invites, inviteId, "invite");

    invite.email = optionalString(payload?.email, invite.email).toLowerCase();
    invite.role = pickEnum(payload?.role, membershipRoles, invite.role);
    invite.expiresAt = optionalString(payload?.expiresAt, invite.expiresAt);
    invite.status = pickEnum(payload?.status, new Set(["created", "sent", "accepted", "expired", "revoked"]), invite.status);
    invite.updatedAt = isoNow();

    addAuditEvent(data, actor, {
      action: "Обновлён инвайт",
      objectType: "invite",
      objectName: invite.email,
      clientId: invite.clientId,
      result: "success",
    });
    markPendingSync(data, invite, "invite", invite.email);

    await writeData(data);
    return { invite, data };
  }

  async function deleteInvite(inviteId, identity) {
    const data = readData();
    const actor = resolveActor(data, identity);
    const invite = findById(data.invites, inviteId, "invite");

    data.invites = data.invites.filter((item) => item.id !== inviteId);
    data.syncStatuses = data.syncStatuses.filter((syncStatus) => syncStatus.objectId !== inviteId);
    addAuditEvent(data, actor, {
      action: "Удалён инвайт",
      objectType: "invite",
      objectName: invite.email,
      clientId: invite.clientId,
      result: "warning",
    });

    await writeData(data);
    return { invite, data };
  }

  function getInviteByToken(token) {
    const data = readData();
    const invite = findInviteByToken(data, token);
    const client = findById(data.clients, invite.clientId, "client");

    return {
      invite: toPublicInvite(invite),
      client: toPublicClient(client),
    };
  }

  function prepareInviteRegistration(token, payload = {}) {
    const data = readData();
    const result = applyInviteRegistration(data, token, payload, { commit: false });

    return result;
  }

  async function commitInviteRegistration(token, payload = {}, provisioning) {
    const data = readData();
    const result = applyInviteRegistration(data, token, payload, { commit: true, provisioning });

    await writeData(data);
    return result;
  }

  async function acceptInvite(token, identity) {
    const data = readData();
    const invite = findInviteByToken(data, token);
    const client = findById(data.clients, invite.clientId, "client");
    const email = requireInviteIdentityEmail(identity);
    const now = isoNow();

    if (invite.email.toLowerCase() !== email) {
      throw new Error("Этот инвайт выписан на другую почту");
    }

    if (invite.status === "revoked") {
      throw new Error("Инвайт отозван");
    }

    if (invite.status === "expired" || isInviteExpired(invite)) {
      invite.status = "expired";
      invite.updatedAt = now;
      await writeData(data);
      throw new Error("Срок действия инвайта истёк");
    }

    const actor = resolveActor(data, identity);
    let user = data.users.find((item) => item.email.toLowerCase() === email);

    if (user) {
      user.authentikUserId = optionalString(identity?.sub, user.authentikUserId ?? null);
      user.name = optionalString(identity?.name, user.name);
      user.avatarUrl = nullableStringWithFallback(identity?.avatarUrl, user.avatarUrl ?? null);
      user.globalStatus = "active";
      user.updatedAt = now;
    } else {
      user = {
        id: uniqueId(data.users, "user", email),
        authentikUserId: nullableString(identity?.sub),
        name: optionalString(identity?.name, email.split("@")[0]),
        email,
        phone: null,
        position: null,
        notes: `Создан через инвайт клиента ${client.name}.`,
        avatarUrl: nullableString(identity?.avatarUrl),
        globalStatus: "active",
        createdAt: now,
        updatedAt: now,
      };
      data.users.push(user);
    }

    let membership = data.memberships.find((item) => item.clientId === invite.clientId && item.userId === user.id);

    if (membership) {
      membership.role = invite.role;
      membership.status = "active";
      membership.updatedAt = now;
    } else {
      membership = {
        id: uniqueId(data.memberships, "mem", `${invite.clientId}-${email}`),
        clientId: invite.clientId,
        userId: user.id,
        role: invite.role,
        status: "active",
        createdAt: now,
        updatedAt: now,
      };
      data.memberships.push(membership);
    }

    invite.status = "accepted";
    invite.updatedAt = now;

    addAuditEvent(data, actor, {
      action: "Инвайт принят",
      objectType: "invite",
      objectName: invite.email,
      clientId: client.id,
      result: "success",
      details: `Role: ${invite.role}`,
    });
    markPendingSync(data, user, "user", email);

    await writeData(data);
    return { invite, client, user, membership, data };
  }

  async function createGroup(payload, identity) {
    const data = readData();
    const actor = resolveActor(data, identity);
    const now = isoNow();
    const clientId = requireString(payload?.clientId, "clientId");
    const client = findById(data.clients, clientId, "client");
    const groupName = optionalString(payload?.name, "Новая группа");
    const group = {
      id: uniqueId(data.groups, "group", `${clientId}-${groupName}`),
      clientId,
      name: groupName,
      description: nullableString(payload?.description),
      memberIds: Array.isArray(payload?.memberIds) ? payload.memberIds.filter((userId) => data.users.some((user) => user.id === userId)) : [],
      createdAt: now,
      updatedAt: now,
    };

    data.groups.push(group);
    addAuditEvent(data, actor, {
      action: "Создана группа",
      objectType: "group",
      objectName: group.name,
      clientId: client.id,
      result: "success",
    });
    markPendingSync(data, group, "group");

    await writeData(data);
    return { group, data };
  }

  async function updateGroup(groupId, payload, identity) {
    const data = readData();
    const actor = resolveActor(data, identity);
    const group = findById(data.groups, groupId, "group");

    group.name = optionalString(payload?.name, group.name);
    group.description = nullableStringWithFallback(payload?.description, group.description ?? null);
    group.memberIds = Array.isArray(payload?.memberIds)
      ? payload.memberIds.filter((userId) => data.users.some((user) => user.id === userId))
      : group.memberIds;
    group.updatedAt = isoNow();

    addAuditEvent(data, actor, {
      action: "Обновлена группа",
      objectType: "group",
      objectName: group.name,
      clientId: group.clientId,
      result: "success",
    });
    markPendingSync(data, group, "group");

    await writeData(data);
    return { group, data };
  }

  async function deleteGroup(groupId, identity) {
    const data = readData();
    const actor = resolveActor(data, identity);
    const group = findById(data.groups, groupId, "group");

    data.groups = data.groups.filter((item) => item.id !== groupId);
    data.grants = data.grants.filter((grant) => !(grant.targetType === "group" && grant.targetId === groupId));
    data.syncStatuses = data.syncStatuses.filter((syncStatus) => syncStatus.objectId !== groupId);
    addAuditEvent(data, actor, {
      action: "Удалена группа",
      objectType: "group",
      objectName: group.name,
      clientId: group.clientId,
      result: "warning",
    });

    await writeData(data);
    return { group, data };
  }

  async function upsertGrant(payload, identity) {
    const data = readData();
    const actor = resolveActor(data, identity);
    const now = isoNow();
    const serviceId = requireString(payload?.serviceId, "serviceId");
    const targetType = pickEnum(payload?.targetType, grantTargetTypes, "client");
    const targetId = requireString(payload?.targetId, "targetId");
    const service = findById(data.services, serviceId, "service");

    assertGrantTargetExists(data, targetType, targetId);

    const existingGrant = data.grants.find(
      (grant) => grant.serviceId === serviceId && grant.targetType === targetType && grant.targetId === targetId
    );
    const grant =
      existingGrant ??
      {
        id: uniqueId(data.grants, "grant", `${service.slug}-${targetType}-${targetId}`),
        serviceId,
        targetType,
        targetId,
        createdAt: now,
      };

    grant.appRole = pickEnum(payload?.appRole, appRoles, existingGrant?.appRole ?? "member");
    grant.status = pickEnum(payload?.status, grantStatuses, existingGrant?.status ?? "active");
    grant.updatedAt = now;

    if (!existingGrant) {
      data.grants.push(grant);
    }

    addAuditEvent(data, actor, {
      action: existingGrant ? "Обновлён доступ" : "Создан доступ",
      objectType: "grant",
      objectName: `${service.slug}:${targetType}:${targetId}`,
      result: "success",
      details: `App role: ${grant.appRole}; status: ${grant.status}`,
    });
    markPendingSync(data, grant, "grant", `${service.slug}:${targetType}:${targetId}`);

    await writeData(data);
    return { grant, data };
  }

  async function upsertException(payload, identity) {
    const data = readData();
    const actor = resolveActor(data, identity);
    const now = isoNow();
    const serviceId = requireString(payload?.serviceId, "serviceId");
    const userId = requireString(payload?.userId, "userId");
    const type = pickEnum(payload?.type, exceptionTypes, "deny");
    const service = findById(data.services, serviceId, "service");
    const user = findById(data.users, userId, "user");

    const existingException = data.exceptions.find(
      (exception) => exception.serviceId === serviceId && exception.userId === userId
    );
    const exception =
      existingException ??
      {
        id: uniqueId(data.exceptions, "exception", `${service.slug}-${user.email}`),
        serviceId,
        userId,
        createdAt: now,
      };

    exception.type = type;
    exception.reason = nullableString(payload?.reason);
    exception.updatedAt = now;

    if (!existingException) {
      data.exceptions.push(exception);
    }

    addAuditEvent(data, actor, {
      action: existingException ? "Обновлено исключение доступа" : "Создано исключение доступа",
      objectType: "exception",
      objectName: `${service.slug}:${user.email}`,
      result: "success",
      details: `Type: ${type}`,
    });
    markPendingSync(data, exception, "grant", `${service.slug}:${user.email}`);

    await writeData(data);
    return { exception, data };
  }

  async function setUserServiceAccess(payload, identity) {
    const data = readData();
    const actor = resolveActor(data, identity);
    const now = isoNow();
    const userId = requireString(payload?.userId, "userId");
    const serviceId = requireString(payload?.serviceId, "serviceId");
    const value = requireString(payload?.value, "value");
    const user = findById(data.users, userId, "user");
    const service = findById(data.services, serviceId, "service");

    const directGrant = data.grants.find(
      (grant) => grant.serviceId === serviceId && grant.targetType === "user" && grant.targetId === userId
    );
    data.grants = data.grants.filter(
      (grant) => !(grant.serviceId === serviceId && grant.targetType === "user" && grant.targetId === userId)
    );
    data.exceptions = data.exceptions.filter((exception) => !(exception.serviceId === serviceId && exception.userId === userId));

    if (value === "deny") {
      data.exceptions.push({
        id: uniqueId(data.exceptions, "exception", `${service.slug}-${user.email}`),
        serviceId,
        userId,
        type: "deny",
        reason: "Создано из матрицы доступа.",
        createdAt: now,
        updatedAt: now,
      });
    } else if (appRoles.has(value) && value !== "owner") {
      data.grants.push({
        id: directGrant?.id ?? uniqueId(data.grants, "grant", `${service.slug}-user-${user.email}`),
        serviceId,
        targetType: "user",
        targetId: userId,
        appRole: value,
        status: "active",
        createdAt: directGrant?.createdAt ?? now,
        updatedAt: now,
      });
    } else if (value !== "unset") {
      throw new Error(`Unsupported access value: ${value}`);
    }

    addAuditEvent(data, actor, {
      action: "Обновлён доступ пользователя к сервису",
      objectType: "grant",
      objectName: `${user.email} / ${service.slug}`,
      result: "success",
      details: `Value: ${value}`,
    });
    markPendingSync(data, { id: `${serviceId}:${userId}` }, "grant", `${service.slug}:${user.email}`);

    await writeData(data);
    return { data };
  }

  async function updateService(serviceId, payload, identity) {
    const data = readData();
    const actor = resolveActor(data, identity);
    const service = findById(data.services, serviceId, "service");

    Object.assign(service, sanitizeServicePatch(payload, service));
    Object.assign(service, syncServiceLaunchLink(service));
    service.updatedAt = isoNow();

    addAuditEvent(data, actor, {
      action: "Обновлён сервис",
      objectType: "service",
      objectName: service.title,
      result: "success",
    });
    markPendingSync(data, service, "service");

    await writeData(data);
    return { service, data };
  }

  async function reorderServices(payload, identity) {
    const data = readData();
    const actor = resolveActor(data, identity);
    const orderedServiceIds = Array.isArray(payload?.orderedServiceIds) ? payload.orderedServiceIds : [];
    const orderById = new Map(orderedServiceIds.map((serviceId, index) => [serviceId, (index + 1) * 10]));
    const now = isoNow();

    data.services = data.services.map((service) => ({
      ...service,
      order: orderById.get(service.id) ?? service.order,
      updatedAt: orderById.has(service.id) ? now : service.updatedAt,
    }));

    addAuditEvent(data, actor, {
      action: "Изменён порядок сервисов",
      objectType: "service",
      objectName: "Каталог сервисов",
      result: "success",
    });

    await writeData(data);
    return { data };
  }

  async function createService(payload, identity) {
    const data = readData();
    const actor = resolveActor(data, identity);
    const now = isoNow();
    const nextIndex = data.services.length + 1;
    const nextOrder = Math.max(0, ...data.services.map((service) => Number(service.order) || 0)) + 10;
    const title = optionalString(payload?.title, "New Service");
    const service = syncServiceLaunchLink({
      id: uniqueId(data.services, "service", title),
      slug: optionalString(payload?.slug, `new-service-${nextIndex}`),
      title,
      subtitle: nullableString(payload?.subtitle) ?? "Новый сервис",
      description: optionalString(payload?.description, "Описание сервиса для витрины."),
      fullDescription: nullableString(payload?.fullDescription) ?? "Заполните описание, медиа и ссылку запуска в редакторе контента.",
      url: optionalString(payload?.url, "https://service.handhdc.ru/sso/launch"),
      launchUrl: nullableString(payload?.launchUrl) ?? optionalString(payload?.url, "https://service.handhdc.ru/sso/launch"),
      accentColor: nullableString(payload?.accentColor) ?? "#F7F8F4",
      fallbackGradient:
        nullableString(payload?.fallbackGradient) ??
        "linear-gradient(135deg, rgba(247, 248, 244, 0.72), rgba(36, 37, 42, 0.9) 52%, #090B0F 88%)",
      coverMediaSource: nullableString(payload?.coverMediaSource) ?? "url",
      coverMediaKind: nullableString(payload?.coverMediaKind) ?? "image",
      ambientMediaSource: nullableString(payload?.ambientMediaSource) ?? "url",
      ambientMediaKind: nullableString(payload?.ambientMediaKind) ?? "gif",
      status: pickEnum(payload?.status, serviceStatuses, "hidden"),
      order: nextOrder,
      authentikApplicationSlug: nullableString(payload?.authentikApplicationSlug) ?? `new-service-${nextIndex}`,
      authentikGroupName: nullableString(payload?.authentikGroupName) ?? `service-new-${nextIndex}`,
      createdAt: now,
      updatedAt: now,
    });

    data.services.push(service);
    addAuditEvent(data, actor, {
      action: "Создан сервис",
      objectType: "service",
      objectName: service.title,
      result: "success",
    });
    markPendingSync(data, service, "service");

    await writeData(data);
    return { service, data };
  }

  async function deleteService(serviceId, identity) {
    const data = readData();
    const actor = resolveActor(data, identity);
    const service = findById(data.services, serviceId, "service");

    data.services = data.services.filter((item) => item.id !== serviceId);
    data.grants = data.grants.filter((grant) => grant.serviceId !== serviceId);
    data.exceptions = data.exceptions.filter((exception) => exception.serviceId !== serviceId);
    data.syncStatuses = data.syncStatuses.filter((syncStatus) => syncStatus.objectId !== serviceId);
    addAuditEvent(data, actor, {
      action: "Удалён сервис",
      objectType: "service",
      objectName: service.title,
      result: "warning",
    });

    await writeData(data);
    return { service, data };
  }

  async function retrySync(syncId, identity) {
    const data = readData();
    const actor = resolveActor(data, identity);
    const syncStatus = findById(data.syncStatuses, syncId, "sync status");

    syncStatus.state = "pending";
    syncStatus.error = null;
    syncStatus.updatedAt = isoNow();
    addAuditEvent(data, actor, {
      action: "Повтор sync",
      objectType: syncStatus.objectType,
      objectName: syncStatus.objectName,
      result: "success",
      details: `Target: ${syncStatus.target}`,
    });

    await writeData(data);
    return { syncStatus, data };
  }

  async function markUserAuthentikProvisioned(userId, provisioning, identity) {
    const data = readData();
    const actor = resolveActor(data, identity);
    const user = findById(data.users, userId, "user");
    const now = isoNow();

    user.authentikUserId = provisioning.authentikUserId ?? user.authentikUserId ?? null;
    user.email = optionalString(provisioning.email, user.email).toLowerCase();
    user.name = optionalString(provisioning.name, user.name);
    user.updatedAt = now;

    const syncStatus = data.syncStatuses.find(
      (status) => status.target === "authentik" && status.objectType === "user" && status.objectId === user.id
    );
    const objectName = user.email;

    if (syncStatus) {
      syncStatus.objectName = objectName;
      syncStatus.state = "synced";
      syncStatus.lastSyncAt = now;
      syncStatus.error = null;
      syncStatus.updatedAt = now;
    } else {
      data.syncStatuses.push({
        id: uniqueId(data.syncStatuses, "sync", `user-${user.id}`),
        objectId: user.id,
        objectName,
        objectType: "user",
        target: "authentik",
        state: "synced",
        lastSyncAt: now,
        error: null,
        updatedAt: now,
      });
    }

    addAuditEvent(data, actor, {
      action: "Пользователь синхронизирован в Authentik",
      objectType: "user",
      objectName,
      result: "success",
      details: `Groups: ${(provisioning.groups ?? []).join(", ") || "none"}`,
    });

    await writeData(data);
    return { user, data };
  }

  function buildAuthentikSyncPlan() {
    const data = readData();

    return {
      mode: "dry-run",
      source: "launcher-control-plane",
      target: "authentik",
      users: data.users.map((user) => ({
        id: user.id,
        authentikUserId: user.authentikUserId ?? null,
        email: user.email,
        name: user.name,
        avatarUrl: user.avatarUrl ?? null,
        active: user.globalStatus === "active",
      })),
      groups: [
        "nodedc:superadmin",
        "nodedc:launcher:admin",
        "nodedc:launcher:user",
        ...data.services.flatMap((service) => (service.authentikGroupName ? [service.authentikGroupName] : [])),
        ...data.groups.map((group) => `client:${group.clientId}:group:${slugify(group.name)}`),
      ],
      accessProjection: {
        services: data.services.length,
        grants: data.grants.filter((grant) => grant.status === "active").length,
        exceptions: data.exceptions.length,
        pendingSyncObjects: data.syncStatuses.filter((syncStatus) => syncStatus.target === "authentik" && syncStatus.state === "pending").length,
      },
    };
  }

  function getWritableStorageRoots() {
    const roots = [publicStorageRoot];

    if (existsSync(join(projectRoot, "dist"))) {
      roots.push(distStorageRoot);
    }

    return roots;
  }

  return {
    buildAuthentikSyncPlan,
    createClient,
    createGroup,
    createInvite,
    createService,
    createUser,
    deleteClient,
    deleteGroup,
    deleteInvite,
    deleteMembership,
    deleteService,
    acceptInvite,
    commitInviteRegistration,
    getInviteByToken,
    getSnapshot,
    prepareInviteRegistration,
    readData,
    replaceData,
    reorderServices,
    retrySync,
    markUserAuthentikProvisioned,
    recordTaskManagerProjectMembership,
    recordTaskManagerWorkspaceMembership,
    removeTaskManagerProjectMembership,
    removeTaskManagerWorkspaceMembership,
    setUserServiceAccess,
    updateClient,
    updateGroup,
    updateInvite,
    updateMembership,
    updateService,
    updateSettings,
    updateUserProfile,
    upsertException,
    upsertGrant,
    writeData,
  };
}

function normalizeData(payload) {
  const data = typeof payload === "object" && payload !== null ? { ...payload } : {};

  for (const key of collectionKeys) {
    if (!Array.isArray(data[key])) {
      data[key] = [];
    }
  }

  data.settings = normalizeSettings(data.settings);
  data.clients = data.clients.map((client) => ({
    ...client,
    integrations: normalizeClientIntegrations(client.integrations),
  }));
  return data;
}

function normalizeSettings(payload) {
  const settings = typeof payload === "object" && payload !== null ? payload : {};
  const brand = typeof settings.brand === "object" && settings.brand !== null ? settings.brand : {};
  const taskManager = typeof settings.taskManager === "object" && settings.taskManager !== null ? settings.taskManager : {};

  return {
    brand: {
      logoLinkUrl: optionalString(brand.logoLinkUrl, defaultSettings.brand.logoLinkUrl),
    },
    taskManager: {
      workspaceCreationPolicy: pickEnum(
        taskManager.workspaceCreationPolicy,
        new Set(["any_authorized_user", "task_admins_only", "disabled"]),
        defaultSettings.taskManager.workspaceCreationPolicy
      ),
    },
  };
}

function normalizeClientIntegrations(payload, fallback = {}) {
  const integrations = typeof payload === "object" && payload !== null ? payload : {};
  const fallbackIntegrations = typeof fallback === "object" && fallback !== null ? fallback : {};
  const taskManager = typeof integrations.taskManager === "object" && integrations.taskManager !== null ? integrations.taskManager : {};
  const fallbackTaskManager =
    typeof fallbackIntegrations.taskManager === "object" && fallbackIntegrations.taskManager !== null ? fallbackIntegrations.taskManager : {};
  const workspaces = normalizeTaskManagerWorkspaces(taskManager, fallbackTaskManager);
  const primaryWorkspace = workspaces.find((workspace) => workspace.isPrimary) ?? workspaces[0] ?? null;

  return {
    taskManager: {
      workspaceSlug: primaryWorkspace?.slug ?? null,
      workspaceName: primaryWorkspace?.name ?? null,
      workspaces,
    },
  };
}

function normalizeTaskManagerWorkspaces(taskManager, fallbackTaskManager = {}) {
  const sourceWorkspaces = Array.isArray(taskManager.workspaces)
    ? taskManager.workspaces
    : Array.isArray(fallbackTaskManager.workspaces)
      ? fallbackTaskManager.workspaces
      : [];
  const legacySlug = nullableStringWithFallback(taskManager.workspaceSlug, fallbackTaskManager.workspaceSlug ?? null);
  const legacyName = nullableStringWithFallback(taskManager.workspaceName, fallbackTaskManager.workspaceName ?? null);
  const bySlug = new Map();

  for (const item of sourceWorkspaces) {
    if (typeof item !== "object" || item === null) continue;
    const slug = nullableString(item.slug);
    if (!slug) continue;
    bySlug.set(slug, {
      slug,
      name: nullableStringWithFallback(item.name, null),
      isPrimary: item.isPrimary === true,
    });
  }

  if (legacySlug && !bySlug.has(legacySlug)) {
    bySlug.set(legacySlug, { slug: legacySlug, name: legacyName, 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 && !primarySeen;
    if (isPrimary) primarySeen = true;
    return {
      slug: workspace.slug,
      name: workspace.name ?? null,
      isPrimary,
    };
  });
}

function normalizeTaskManagerMembershipRole(value) {
  return value === "guest" || value === "admin" || value === "member" ? value : "member";
}

function upsertTaskManagerMembership(data, payload) {
  const workspaceSlug = requireString(payload.workspaceSlug, "workspaceSlug");
  const existingMembership = data.taskManagerMemberships.find(
    (membership) => membership.clientId === payload.clientId && membership.userId === payload.userId && membership.workspaceSlug === workspaceSlug
  );
  const nextMembership = {
    id: existingMembership?.id ?? uniqueId(data.taskManagerMemberships, "tasker_mem", `${payload.clientId}-${payload.userId}-${workspaceSlug}`),
    clientId: payload.clientId,
    userId: payload.userId,
    workspaceSlug,
    workspaceName: nullableStringWithFallback(payload.workspaceName, existingMembership?.workspaceName ?? null),
    role: normalizeTaskManagerMembershipRole(payload.role),
    planeUserId: nullableStringWithFallback(payload.planeUserId, existingMembership?.planeUserId ?? null),
    planeRole: typeof payload.planeRole === "number" ? payload.planeRole : (existingMembership?.planeRole ?? null),
    updatedAt: isoNow(),
  };

  if (existingMembership) {
    Object.assign(existingMembership, nextMembership);
    return existingMembership;
  }

  data.taskManagerMemberships.push(nextMembership);
  return nextMembership;
}

function upsertTaskManagerProjectMembership(data, payload) {
  const workspaceSlug = requireString(payload.workspaceSlug, "workspaceSlug");
  const projectId = requireString(payload.projectId, "projectId");
  const existingMembership = data.taskManagerProjectMemberships.find(
    (membership) =>
      membership.clientId === payload.clientId &&
      membership.userId === payload.userId &&
      membership.workspaceSlug === workspaceSlug &&
      membership.projectId === projectId
  );
  const nextMembership = {
    id:
      existingMembership?.id ??
      uniqueId(data.taskManagerProjectMemberships, "tasker_project_mem", `${payload.clientId}-${payload.userId}-${workspaceSlug}-${projectId}`),
    clientId: payload.clientId,
    userId: payload.userId,
    workspaceSlug,
    workspaceName: nullableStringWithFallback(payload.workspaceName, existingMembership?.workspaceName ?? null),
    projectId,
    projectIdentifier: nullableStringWithFallback(payload.projectIdentifier, existingMembership?.projectIdentifier ?? null),
    projectName: nullableStringWithFallback(payload.projectName, existingMembership?.projectName ?? null),
    role: normalizeTaskManagerMembershipRole(payload.role),
    planeUserId: nullableStringWithFallback(payload.planeUserId, existingMembership?.planeUserId ?? null),
    planeRole: typeof payload.planeRole === "number" ? payload.planeRole : (existingMembership?.planeRole ?? null),
    updatedAt: isoNow(),
  };

  if (existingMembership) {
    Object.assign(existingMembership, nextMembership);
    return existingMembership;
  }

  data.taskManagerProjectMemberships.push(nextMembership);
  return nextMembership;
}

function resolveActor(data, identity) {
  const user = data.users.find(
    (item) =>
      (identity?.sub && item.authentikUserId === identity.sub) ||
      (identity?.email && item.email.toLowerCase() === identity.email.toLowerCase())
  );

  if (user) {
    return {
      id: user.id,
      name: user.name,
      email: user.email,
      source: "launcher",
    };
  }

  return {
    id: identity?.sub ? `oidc:${identity.sub}` : "system",
    name: identity?.name || identity?.email || "System",
    email: identity?.email || null,
    source: "oidc",
  };
}

function addAuditEvent(data, actor, event) {
  data.auditEvents.push({
    id: uniqueId(data.auditEvents, "audit", event.objectName ?? event.objectType ?? "event"),
    at: isoNow(),
    actorUserId: actor.id,
    actorName: actor.name,
    action: event.action,
    objectType: event.objectType,
    objectName: event.objectName,
    clientId: event.clientId ?? null,
    result: event.result ?? "success",
    details: event.details ?? null,
  });
}

function markPendingSync(data, object, objectType, objectName = object.name ?? object.email ?? object.id) {
  const now = isoNow();
  const existingStatus = data.syncStatuses.find(
    (status) => status.target === "authentik" && status.objectType === objectType && status.objectId === object.id
  );

  if (existingStatus) {
    existingStatus.objectName = objectName;
    existingStatus.state = "pending";
    existingStatus.error = null;
    existingStatus.updatedAt = now;
    return;
  }

  data.syncStatuses.push({
    id: uniqueId(data.syncStatuses, "sync", `${objectType}-${object.id}`),
    objectId: object.id,
    objectName,
    objectType,
    target: "authentik",
    state: "pending",
    lastSyncAt: null,
    error: null,
    updatedAt: now,
  });
}

async function writeJsonAtomically(filePath, payload) {
  const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;

  await writeFile(tempPath, payload, "utf8");
  await rename(tempPath, filePath);
}

function assertGrantTargetExists(data, targetType, targetId) {
  if (targetType === "client") {
    findById(data.clients, targetId, "client");
  } else if (targetType === "group") {
    findById(data.groups, targetId, "group");
  } else {
    findById(data.users, targetId, "user");
  }
}

function findInviteByToken(data, token) {
  const normalizedToken = requireString(token, "token");
  const invite = data.invites.find((candidate) => candidate.token === normalizedToken);

  if (!invite) {
    throw new Error("Инвайт не найден");
  }

  return invite;
}

function requireInviteIdentityEmail(identity) {
  const email = typeof identity?.email === "string" ? identity.email.trim().toLowerCase() : "";

  if (!email) {
    throw new Error("Для принятия инвайта нужна подтверждённая почта");
  }

  return email;
}

function isInviteExpired(invite) {
  if (!invite.expiresAt) return false;
  return Number.isFinite(Date.parse(invite.expiresAt)) && Date.parse(invite.expiresAt) <= Date.now();
}

function toPublicInvite(invite) {
  return {
    id: invite.id,
    role: invite.role,
    expiresAt: invite.expiresAt,
    status: isInviteExpired(invite) && invite.status !== "accepted" && invite.status !== "revoked" ? "expired" : invite.status,
  };
}

function toPublicClient(client) {
  return {
    id: client.id,
    name: client.name,
    status: client.status,
  };
}

function applyInviteRegistration(data, token, payload, { commit, provisioning = null } = {}) {
  const invite = findInviteByToken(data, token);
  const client = findById(data.clients, invite.clientId, "client");
  const now = isoNow();
  const requestedEmail = normalizeInviteRegistrationEmail(payload?.email);
  const email = invite.email.toLowerCase();
  const name = optionalString(payload?.name, requestedEmail.split("@")[0]);

  validateInviteCanBeRegistered(invite);

  if (!requestedEmail || requestedEmail !== email) {
    throw new Error("Для этой почты нет активного инвайта");
  }

  let user = data.users.find((item) => item.email.toLowerCase() === email);

  if (user?.authentikUserId && !provisioning) {
    throw new Error("Аккаунт уже существует. Войдите под почтой инвайта.");
  }

  if (user) {
    user.name = name;
    user.globalStatus = "active";
    user.authentikUserId = provisioning?.authentikUserId ?? user.authentikUserId ?? null;
    user.updatedAt = now;
  } else {
    user = {
      id: uniqueId(data.users, "user", email),
      authentikUserId: provisioning?.authentikUserId ?? null,
      name,
      email,
      phone: null,
      position: null,
      notes: `Создан через публичную регистрацию по инвайту клиента ${client.name}.`,
      avatarUrl: null,
      globalStatus: "active",
      createdAt: now,
      updatedAt: now,
    };
    data.users.push(user);
  }

  let membership = data.memberships.find((item) => item.clientId === invite.clientId && item.userId === user.id);

  if (membership) {
    membership.role = invite.role;
    membership.status = "active";
    membership.updatedAt = now;
  } else {
    membership = {
      id: uniqueId(data.memberships, "mem", `${invite.clientId}-${email}`),
      clientId: invite.clientId,
      userId: user.id,
      role: invite.role,
      status: "active",
      createdAt: now,
      updatedAt: now,
    };
    data.memberships.push(membership);
  }

  invite.status = "accepted";
  invite.updatedAt = now;
  markPendingSync(data, user, "user", email);

  if (commit) {
    addAuditEvent(data, { id: user.id, name: user.name, email: user.email, source: "invite" }, {
      action: "Регистрация по инвайту",
      objectType: "invite",
      objectName: invite.email,
      clientId: client.id,
      result: "success",
      details: `Role: ${invite.role}`,
    });
  }

  return { invite, client, user, membership, data };
}

function normalizeInviteRegistrationEmail(value) {
  return typeof value === "string" ? value.trim().toLowerCase() : "";
}

function validateInviteCanBeRegistered(invite) {
  if (invite.status === "accepted") {
    throw new Error("Инвайт уже принят");
  }

  if (invite.status === "revoked") {
    throw new Error("Инвайт отозван");
  }

  if (invite.status === "expired" || isInviteExpired(invite)) {
    throw new Error("Срок действия инвайта истёк");
  }
}

function findById(items, id, label) {
  const item = items.find((candidate) => candidate.id === id);

  if (!item) {
    throw new Error(`Unknown ${label}: ${id}`);
  }

  return item;
}

function requireString(value, fieldName) {
  if (typeof value !== "string" || !value.trim()) {
    throw new Error(`Field ${fieldName} is required`);
  }

  return value.trim();
}

function optionalString(value, fallback) {
  return typeof value === "string" && value.trim() ? value.trim() : fallback;
}

function nullableString(value) {
  return typeof value === "string" && value.trim() ? value.trim() : null;
}

function nullableStringWithFallback(value, fallback) {
  return value === undefined ? fallback : nullableString(value);
}

function pickEnum(value, allowedValues, fallback) {
  return typeof value === "string" && allowedValues.has(value) ? value : fallback;
}

function uniqueId(items, prefix, seed) {
  const base = `${prefix}_${slugify(seed)}`;
  let candidate = base;
  let counter = 1;
  const ids = new Set(items.map((item) => item.id));

  while (ids.has(candidate)) {
    counter += 1;
    candidate = `${base}_${counter}`;
  }

  return candidate;
}

function sanitizeServicePatch(payload, service) {
  const patch = {};
  const stringFields = [
    "slug",
    "title",
    "subtitle",
    "description",
    "fullDescription",
    "url",
    "launchUrl",
    "iconUrl",
    "coverImageUrl",
    "coverMediaKind",
    "coverMediaSource",
    "coverMediaFileName",
    "previewVideoUrl",
    "ambientVideoUrl",
    "ambientMediaKind",
    "ambientMediaSource",
    "ambientMediaFileName",
    "accentColor",
    "fallbackGradient",
    "authentikApplicationSlug",
    "authentikGroupName",
  ];

  for (const field of stringFields) {
    if (field in (payload ?? {})) {
      patch[field] = nullableStringWithFallback(payload[field], service[field] ?? null);
    }
  }

  if (typeof payload?.order === "number") {
    patch.order = payload.order;
  }

  if ("isAvailableForAllNewClients" in (payload ?? {})) {
    patch.isAvailableForAllNewClients = Boolean(payload.isAvailableForAllNewClients);
  }

  patch.status = pickEnum(payload?.status, serviceStatuses, service.status);
  return patch;
}

function syncServiceLaunchLink(service) {
  const launchLink = String(service.launchUrl || service.url || "").trim();

  return {
    ...service,
    url: launchLink,
    launchUrl: launchLink || null,
  };
}

function slugify(value) {
  const slug =
    String(value)
      .normalize("NFKD")
      .replace(/[^\w]+/g, "_")
      .replace(/^_+|_+$/g, "")
      .toLowerCase()
      .slice(0, 80) || randomUUID().slice(0, 8);

  return slug;
}

function isoNow() {
  return new Date().toISOString();
}
