import { Router, type IRouter } from "express";
import { eq, ilike, inArray, sql } from "drizzle-orm";
import { db } from "@workspace/db";
import {
  groupsTable,
  userGroupsTable,
  groupAppsTable,
  groupRolesTable,
  usersTable,
  appsTable,
  rolesTable,
  auditLogsTable,
} from "@workspace/db";
import { requireAdmin } from "../middlewares/auth";
import { emitAppsChangedToUsers } from "../lib/realtime";
import {
  listPermissionAudit,
  parsePermissionAuditFilters,
  recordPermissionAudit,
} from "../lib/permission-audit";
import type { PermissionAuditChangeKind } from "@workspace/db";
import {
  CreateGroupBody,
  UpdateGroupBody,
} from "@workspace/api-zod";

async function getGroupMemberIds(groupId: number): Promise<number[]> {
  const rows = await db
    .select({ userId: userGroupsTable.userId })
    .from(userGroupsTable)
    .where(eq(userGroupsTable.groupId, groupId));
  return rows.map((r) => r.userId);
}

const router: IRouter = Router();

function serializeGroup(g: typeof groupsTable.$inferSelect) {
  return {
    id: g.id,
    name: g.name,
    descriptionAr: g.descriptionAr,
    descriptionEn: g.descriptionEn,
    isSystem: Boolean(g.isSystem),
    createdAt: g.createdAt,
  };
}

router.get("/groups", requireAdmin, async (req, res): Promise<void> => {
  const q = typeof req.query.q === "string" ? req.query.q.trim() : "";
  const where = q ? ilike(groupsTable.name, `%${q}%`) : undefined;

  const rows = where
    ? await db.select().from(groupsTable).where(where).orderBy(groupsTable.name)
    : await db.select().from(groupsTable).orderBy(groupsTable.name);

  if (rows.length === 0) {
    res.json([]);
    return;
  }

  const groupIds = rows.map((r) => r.id);

  const memberCounts = await db
    .select({
      groupId: userGroupsTable.groupId,
      count: sql<number>`count(*)::int`,
    })
    .from(userGroupsTable)
    .where(inArray(userGroupsTable.groupId, groupIds))
    .groupBy(userGroupsTable.groupId);

  const appCounts = await db
    .select({
      groupId: groupAppsTable.groupId,
      count: sql<number>`count(*)::int`,
    })
    .from(groupAppsTable)
    .where(inArray(groupAppsTable.groupId, groupIds))
    .groupBy(groupAppsTable.groupId);

  const roleCounts = await db
    .select({
      groupId: groupRolesTable.groupId,
      count: sql<number>`count(*)::int`,
    })
    .from(groupRolesTable)
    .where(inArray(groupRolesTable.groupId, groupIds))
    .groupBy(groupRolesTable.groupId);

  const memberMap = new Map(memberCounts.map((m) => [m.groupId, m.count]));
  const appMap = new Map(appCounts.map((m) => [m.groupId, m.count]));
  const roleMap = new Map(roleCounts.map((m) => [m.groupId, m.count]));

  res.json(
    rows.map((g) => ({
      ...serializeGroup(g),
      memberCount: memberMap.get(g.id) ?? 0,
      appCount: appMap.get(g.id) ?? 0,
      roleCount: roleMap.get(g.id) ?? 0,
    })),
  );
});

router.get(
  "/groups/:id/audit",
  requireAdmin,
  async (req, res): Promise<void> => {
    const id = Number(req.params.id);
    if (!Number.isInteger(id)) {
      res.status(400).json({ error: "Invalid id" });
      return;
    }
    const limitRaw = Number(req.query.limit);
    const offsetRaw = Number(req.query.offset);
    const limit =
      Number.isFinite(limitRaw) && limitRaw > 0
        ? Math.min(Math.floor(limitRaw), 200)
        : 10;
    const offset =
      Number.isFinite(offsetRaw) && offsetRaw >= 0 ? Math.floor(offsetRaw) : 0;
    const [group] = await db
      .select({ id: groupsTable.id })
      .from(groupsTable)
      .where(eq(groupsTable.id, id));
    if (!group) {
      res.status(404).json({ error: "Group not found" });
      return;
    }
    const filters = parsePermissionAuditFilters(req, res);
    if (!filters) return;
    const result = await listPermissionAudit({
      targetKind: "group",
      targetId: id,
      limit,
      offset,
      filters,
    });
    res.json(result);
  },
);

router.get("/groups/:id", requireAdmin, async (req, res): Promise<void> => {
  const id = Number(req.params.id);
  if (!Number.isInteger(id)) {
    res.status(400).json({ error: "Invalid id" });
    return;
  }
  const [group] = await db.select().from(groupsTable).where(eq(groupsTable.id, id));
  if (!group) {
    res.status(404).json({ error: "Group not found" });
    return;
  }
  const apps = await db
    .select({ appId: groupAppsTable.appId })
    .from(groupAppsTable)
    .where(eq(groupAppsTable.groupId, id));
  const roles = await db
    .select({ roleId: groupRolesTable.roleId })
    .from(groupRolesTable)
    .where(eq(groupRolesTable.groupId, id));
  const users = await db
    .select({ userId: userGroupsTable.userId })
    .from(userGroupsTable)
    .where(eq(userGroupsTable.groupId, id));
  res.json({
    ...serializeGroup(group),
    appIds: apps.map((a) => a.appId),
    roleIds: roles.map((r) => r.roleId),
    userIds: users.map((u) => u.userId),
  });
});

async function validateAssignmentIds(
  appIds: number[] | undefined,
  roleIds: number[] | undefined,
  userIds: number[] | undefined,
): Promise<{ ok: true } | { ok: false; error: string }> {
  if (appIds !== undefined && appIds.length > 0) {
    const found = await db
      .select({ id: appsTable.id })
      .from(appsTable)
      .where(inArray(appsTable.id, appIds));
    if (found.length !== new Set(appIds).size) {
      return { ok: false, error: "One or more appIds do not exist" };
    }
  }
  if (roleIds !== undefined && roleIds.length > 0) {
    const found = await db
      .select({ id: rolesTable.id })
      .from(rolesTable)
      .where(inArray(rolesTable.id, roleIds));
    if (found.length !== new Set(roleIds).size) {
      return { ok: false, error: "One or more roleIds do not exist" };
    }
  }
  if (userIds !== undefined && userIds.length > 0) {
    const found = await db
      .select({ id: usersTable.id })
      .from(usersTable)
      .where(inArray(usersTable.id, userIds));
    if (found.length !== new Set(userIds).size) {
      return { ok: false, error: "One or more userIds do not exist" };
    }
  }
  return { ok: true };
}

type Tx = Parameters<Parameters<typeof db.transaction>[0]>[0];

async function applyGroupAssignmentsTx(
  tx: Tx,
  groupId: number,
  appIds: number[] | undefined,
  roleIds: number[] | undefined,
  userIds: number[] | undefined,
  actorUserId: number | null,
) {
  if (appIds !== undefined) {
    const previousIds = (
      await tx
        .select({ appId: groupAppsTable.appId })
        .from(groupAppsTable)
        .where(eq(groupAppsTable.groupId, groupId))
    ).map((r) => r.appId);
    await tx.delete(groupAppsTable).where(eq(groupAppsTable.groupId, groupId));
    if (appIds.length > 0) {
      await tx
        .insert(groupAppsTable)
        .values(appIds.map((appId) => ({ groupId, appId })))
        .onConflictDoNothing();
    }
    await recordPermissionAudit(tx, {
      targetKind: "group",
      targetId: groupId,
      changeKind: "group.apps",
      actorUserId,
      previousIds,
      newIds: appIds,
    });
  }
  if (roleIds !== undefined) {
    const previousIds = (
      await tx
        .select({ roleId: groupRolesTable.roleId })
        .from(groupRolesTable)
        .where(eq(groupRolesTable.groupId, groupId))
    ).map((r) => r.roleId);
    await tx.delete(groupRolesTable).where(eq(groupRolesTable.groupId, groupId));
    if (roleIds.length > 0) {
      await tx
        .insert(groupRolesTable)
        .values(roleIds.map((roleId) => ({ groupId, roleId })))
        .onConflictDoNothing();
    }
    await recordPermissionAudit(tx, {
      targetKind: "group",
      targetId: groupId,
      changeKind: "group.roles",
      actorUserId,
      previousIds,
      newIds: roleIds,
    });
  }
  if (userIds !== undefined) {
    const previousIds = (
      await tx
        .select({ userId: userGroupsTable.userId })
        .from(userGroupsTable)
        .where(eq(userGroupsTable.groupId, groupId))
    ).map((r) => r.userId);
    await tx.delete(userGroupsTable).where(eq(userGroupsTable.groupId, groupId));
    if (userIds.length > 0) {
      await tx
        .insert(userGroupsTable)
        .values(userIds.map((userId) => ({ userId, groupId })))
        .onConflictDoNothing();
    }
    await recordPermissionAudit(tx, {
      targetKind: "group",
      targetId: groupId,
      changeKind: "group.users",
      actorUserId,
      previousIds,
      newIds: userIds,
    });
    // Mirror onto each affected user so their personal history shows the
    // group-membership change regardless of which editor was used.
    const added = userIds.filter((uid) => !previousIds.includes(uid));
    const removed = previousIds.filter((uid) => !userIds.includes(uid));
    for (const uid of [...added, ...removed]) {
      const newGroupIds = (
        await tx
          .select({ groupId: userGroupsTable.groupId })
          .from(userGroupsTable)
          .where(eq(userGroupsTable.userId, uid))
      ).map((r) => r.groupId);
      const previousGroupIds = added.includes(uid)
        ? newGroupIds.filter((g) => g !== groupId)
        : [...newGroupIds, groupId];
      await recordPermissionAudit(tx, {
        targetKind: "user",
        targetId: uid,
        changeKind: "user.groups",
        actorUserId,
        previousIds: previousGroupIds,
        newIds: newGroupIds,
      });
    }
  }
}

router.post("/groups", requireAdmin, async (req, res): Promise<void> => {
  const parsed = CreateGroupBody.safeParse(req.body);
  if (!parsed.success) {
    res.status(400).json({ error: parsed.error.message });
    return;
  }
  const existing = await db
    .select({ id: groupsTable.id })
    .from(groupsTable)
    .where(eq(groupsTable.name, parsed.data.name));
  if (existing.length > 0) {
    res.status(409).json({ error: "Group name already taken" });
    return;
  }
  const validation = await validateAssignmentIds(
    parsed.data.appIds,
    parsed.data.roleIds,
    parsed.data.userIds,
  );
  if (!validation.ok) {
    res.status(400).json({ error: validation.error });
    return;
  }
  const group = await db.transaction(async (tx) => {
    const [g] = await tx
      .insert(groupsTable)
      .values({
        name: parsed.data.name,
        descriptionAr: parsed.data.descriptionAr ?? null,
        descriptionEn: parsed.data.descriptionEn ?? null,
      })
      .returning();
    await applyGroupAssignmentsTx(
      tx,
      g.id,
      parsed.data.appIds,
      parsed.data.roleIds,
      parsed.data.userIds,
      req.session.userId ?? null,
    );
    return g;
  });
  await db.insert(auditLogsTable).values({
    actorUserId: req.session.userId ?? null,
    action: "group.create",
    targetType: "group",
    targetId: group.id,
    metadata: {
      name: group.name,
      descriptionAr: group.descriptionAr,
      descriptionEn: group.descriptionEn,
      memberCount: parsed.data.userIds?.length ?? 0,
      appCount: parsed.data.appIds?.length ?? 0,
      roleCount: parsed.data.roleIds?.length ?? 0,
    },
  });
  res.status(201).json({
    ...serializeGroup(group),
    memberCount: parsed.data.userIds?.length ?? 0,
    appCount: parsed.data.appIds?.length ?? 0,
    roleCount: parsed.data.roleIds?.length ?? 0,
  });
});

router.patch("/groups/:id", requireAdmin, async (req, res): Promise<void> => {
  const id = Number(req.params.id);
  if (!Number.isInteger(id)) {
    res.status(400).json({ error: "Invalid id" });
    return;
  }
  const parsed = UpdateGroupBody.safeParse(req.body);
  if (!parsed.success) {
    res.status(400).json({ error: parsed.error.message });
    return;
  }
  const [existing] = await db.select().from(groupsTable).where(eq(groupsTable.id, id));
  if (!existing) {
    res.status(404).json({ error: "Group not found" });
    return;
  }
  const validation = await validateAssignmentIds(
    parsed.data.appIds,
    parsed.data.roleIds,
    parsed.data.userIds,
  );
  if (!validation.ok) {
    res.status(400).json({ error: validation.error });
    return;
  }
  const previousMemberIds = await getGroupMemberIds(id);
  const previousAppIds = (
    await db
      .select({ appId: groupAppsTable.appId })
      .from(groupAppsTable)
      .where(eq(groupAppsTable.groupId, id))
  ).map((r) => r.appId);
  const previousRoleIds = (
    await db
      .select({ roleId: groupRolesTable.roleId })
      .from(groupRolesTable)
      .where(eq(groupRolesTable.groupId, id))
  ).map((r) => r.roleId);
  const updates: Partial<typeof groupsTable.$inferInsert> = {};
  if (parsed.data.name !== undefined) updates.name = parsed.data.name;
  if (parsed.data.descriptionAr !== undefined) updates.descriptionAr = parsed.data.descriptionAr;
  if (parsed.data.descriptionEn !== undefined) updates.descriptionEn = parsed.data.descriptionEn;
  await db.transaction(async (tx) => {
    if (Object.keys(updates).length > 0) {
      await tx.update(groupsTable).set(updates).where(eq(groupsTable.id, id));
    }
    await applyGroupAssignmentsTx(
      tx,
      id,
      parsed.data.appIds,
      parsed.data.roleIds,
      parsed.data.userIds,
      req.session.userId ?? null,
    );
  });

  const [group] = await db.select().from(groupsTable).where(eq(groupsTable.id, id));
  const apps = await db
    .select({ appId: groupAppsTable.appId })
    .from(groupAppsTable)
    .where(eq(groupAppsTable.groupId, id));
  const roles = await db
    .select({ roleId: groupRolesTable.roleId })
    .from(groupRolesTable)
    .where(eq(groupRolesTable.groupId, id));
  const users = await db
    .select({ userId: userGroupsTable.userId })
    .from(userGroupsTable)
    .where(eq(userGroupsTable.groupId, id));

  const currentMemberIds = users.map((u) => u.userId);
  const currentAppIds = apps.map((a) => a.appId);
  const currentRoleIds = roles.map((r) => r.roleId);
  const membershipChanged = parsed.data.userIds !== undefined;
  const grantsChanged =
    parsed.data.appIds !== undefined || parsed.data.roleIds !== undefined;
  const affected = new Set<number>();
  if (membershipChanged) {
    for (const uid of previousMemberIds) affected.add(uid);
    for (const uid of currentMemberIds) affected.add(uid);
  } else if (grantsChanged) {
    for (const uid of currentMemberIds) affected.add(uid);
  }
  if (affected.size > 0) {
    await emitAppsChangedToUsers(Array.from(affected));
  }

  function diff(prev: number[], next: number[]) {
    const prevSet = new Set(prev);
    const nextSet = new Set(next);
    return {
      added: next.filter((n) => !prevSet.has(n)),
      removed: prev.filter((n) => !nextSet.has(n)),
    };
  }

  const fieldsChanged = Object.keys(updates).length > 0;
  if (fieldsChanged || membershipChanged || grantsChanged) {
    const meta: Record<string, unknown> = {
      name: group.name,
    };
    if (fieldsChanged) {
      meta.previousName = existing.name;
      meta.changes = updates;
    }
    if (membershipChanged) {
      meta.members = diff(previousMemberIds, currentMemberIds);
    }
    if (parsed.data.appIds !== undefined) {
      meta.apps = diff(previousAppIds, currentAppIds);
    }
    if (parsed.data.roleIds !== undefined) {
      meta.roles = diff(previousRoleIds, currentRoleIds);
    }
    await db.insert(auditLogsTable).values({
      actorUserId: req.session.userId ?? null,
      action: "group.update",
      targetType: "group",
      targetId: id,
      metadata: meta,
    });
  }

  res.json({
    ...serializeGroup(group),
    appIds: apps.map((a) => a.appId),
    roleIds: roles.map((r) => r.roleId),
    userIds: users.map((u) => u.userId),
  });
});

// Sub-resource assignment endpoints — explicit add/remove for users, apps, roles.
// These complement the aggregate PATCH /groups/:id by exposing single-link writes.
type SubKind = "users" | "apps" | "roles";

function isSubKind(k: string): k is SubKind {
  return k === "users" || k === "apps" || k === "roles";
}

async function ensureGroup(id: number) {
  const [g] = await db.select().from(groupsTable).where(eq(groupsTable.id, id));
  return g;
}

// Capture the human-readable name for the linked record so audit log entries
// remain meaningful even after the user/app/role is later deleted.
async function loadSubResourceNameFields(
  kind: SubKind,
  targetId: number,
): Promise<Record<string, unknown> | null> {
  if (kind === "users") {
    const [row] = await db
      .select({ id: usersTable.id, username: usersTable.username })
      .from(usersTable)
      .where(eq(usersTable.id, targetId));
    if (!row) return null;
    return { username: row.username };
  }
  if (kind === "apps") {
    const [row] = await db
      .select({
        id: appsTable.id,
        slug: appsTable.slug,
        nameAr: appsTable.nameAr,
        nameEn: appsTable.nameEn,
      })
      .from(appsTable)
      .where(eq(appsTable.id, targetId));
    if (!row) return null;
    return { appSlug: row.slug, appNameAr: row.nameAr, appNameEn: row.nameEn };
  }
  const [row] = await db
    .select({ id: rolesTable.id, name: rolesTable.name })
    .from(rolesTable)
    .where(eq(rolesTable.id, targetId));
  if (!row) return null;
  return { roleName: row.name };
}

router.post("/groups/:id/:kind/:targetId", requireAdmin, async (req, res): Promise<void> => {
  const id = Number(req.params.id);
  const targetId = Number(req.params.targetId);
  const kind = String(req.params.kind);
  if (!Number.isInteger(id) || !Number.isInteger(targetId) || !isSubKind(kind)) {
    res.status(400).json({ error: "Invalid id" });
    return;
  }
  const group = await ensureGroup(id);
  if (!group) {
    res.status(404).json({ error: "Group not found" });
    return;
  }
  const nameFields = await loadSubResourceNameFields(kind, targetId);
  if (!nameFields) {
    res.status(404).json({ error: `${kind.slice(0, -1)} not found` });
    return;
  }
  const auditChangeKind: PermissionAuditChangeKind =
    kind === "users"
      ? "group.users"
      : kind === "apps"
        ? "group.apps"
        : "group.roles";
  const inserted = await db.transaction(async (tx) => {
    let previousIds: number[] = [];
    let ins: { groupId: number }[] = [];
    if (kind === "users") {
      previousIds = (
        await tx
          .select({ userId: userGroupsTable.userId })
          .from(userGroupsTable)
          .where(eq(userGroupsTable.groupId, id))
      ).map((r) => r.userId);
      ins = await tx
        .insert(userGroupsTable)
        .values({ groupId: id, userId: targetId })
        .onConflictDoNothing()
        .returning({ groupId: userGroupsTable.groupId });
    } else if (kind === "apps") {
      previousIds = (
        await tx
          .select({ appId: groupAppsTable.appId })
          .from(groupAppsTable)
          .where(eq(groupAppsTable.groupId, id))
      ).map((r) => r.appId);
      ins = await tx
        .insert(groupAppsTable)
        .values({ groupId: id, appId: targetId })
        .onConflictDoNothing()
        .returning({ groupId: groupAppsTable.groupId });
    } else {
      previousIds = (
        await tx
          .select({ roleId: groupRolesTable.roleId })
          .from(groupRolesTable)
          .where(eq(groupRolesTable.groupId, id))
      ).map((r) => r.roleId);
      ins = await tx
        .insert(groupRolesTable)
        .values({ groupId: id, roleId: targetId })
        .onConflictDoNothing()
        .returning({ groupId: groupRolesTable.groupId });
    }
    if (ins.length > 0) {
      await recordPermissionAudit(tx, {
        targetKind: "group",
        targetId: id,
        changeKind: auditChangeKind,
        actorUserId: req.session.userId ?? null,
        previousIds,
        newIds: previousIds.includes(targetId)
          ? previousIds
          : [...previousIds, targetId],
      });
      if (kind === "users") {
        // Mirror onto the affected user's history.
        const newGroupIds = (
          await tx
            .select({ groupId: userGroupsTable.groupId })
            .from(userGroupsTable)
            .where(eq(userGroupsTable.userId, targetId))
        ).map((r) => r.groupId);
        await recordPermissionAudit(tx, {
          targetKind: "user",
          targetId,
          changeKind: "user.groups",
          actorUserId: req.session.userId ?? null,
          previousIds: newGroupIds.filter((g) => g !== id),
          newIds: newGroupIds,
        });
      }
    }
    return ins;
  });
  if (inserted.length > 0) {
    if (kind === "users") {
      await emitAppsChangedToUsers([targetId]);
    } else {
      await emitAppsChangedToUsers(await getGroupMemberIds(id));
    }
  }
  if (inserted.length > 0) {
    const subject = kind === "users" ? "user" : kind === "apps" ? "app" : "role";
    await db.insert(auditLogsTable).values({
      actorUserId: req.session.userId ?? null,
      action: `group.${subject}.add`,
      targetType: "group",
      targetId: id,
      metadata: {
        groupName: group.name,
        [`${subject}Id`]: targetId,
        ...nameFields,
      },
    });
  }
  res.sendStatus(204);
});

router.delete("/groups/:id/:kind/:targetId", requireAdmin, async (req, res): Promise<void> => {
  const id = Number(req.params.id);
  const targetId = Number(req.params.targetId);
  const kind = String(req.params.kind);
  if (!Number.isInteger(id) || !Number.isInteger(targetId) || !isSubKind(kind)) {
    res.status(400).json({ error: "Invalid id" });
    return;
  }
  const group = await ensureGroup(id);
  if (!group) {
    res.status(404).json({ error: "Group not found" });
    return;
  }
  const auditChangeKind: PermissionAuditChangeKind =
    kind === "users"
      ? "group.users"
      : kind === "apps"
        ? "group.apps"
        : "group.roles";
  const memberIdsBefore =
    kind === "users" ? null : await getGroupMemberIds(id);
  const removed = await db.transaction(async (tx) => {
    let previousIds: number[] = [];
    let del: { groupId: number }[] = [];
    if (kind === "users") {
      previousIds = (
        await tx
          .select({ userId: userGroupsTable.userId })
          .from(userGroupsTable)
          .where(eq(userGroupsTable.groupId, id))
      ).map((r) => r.userId);
      del = await tx
        .delete(userGroupsTable)
        .where(sql`${userGroupsTable.groupId} = ${id} and ${userGroupsTable.userId} = ${targetId}`)
        .returning({ groupId: userGroupsTable.groupId });
    } else if (kind === "apps") {
      previousIds = (
        await tx
          .select({ appId: groupAppsTable.appId })
          .from(groupAppsTable)
          .where(eq(groupAppsTable.groupId, id))
      ).map((r) => r.appId);
      del = await tx
        .delete(groupAppsTable)
        .where(sql`${groupAppsTable.groupId} = ${id} and ${groupAppsTable.appId} = ${targetId}`)
        .returning({ groupId: groupAppsTable.groupId });
    } else {
      previousIds = (
        await tx
          .select({ roleId: groupRolesTable.roleId })
          .from(groupRolesTable)
          .where(eq(groupRolesTable.groupId, id))
      ).map((r) => r.roleId);
      del = await tx
        .delete(groupRolesTable)
        .where(sql`${groupRolesTable.groupId} = ${id} and ${groupRolesTable.roleId} = ${targetId}`)
        .returning({ groupId: groupRolesTable.groupId });
    }
    if (del.length > 0) {
      await recordPermissionAudit(tx, {
        targetKind: "group",
        targetId: id,
        changeKind: auditChangeKind,
        actorUserId: req.session.userId ?? null,
        previousIds,
        newIds: previousIds.filter((pid) => pid !== targetId),
      });
      if (kind === "users") {
        // Mirror onto the affected user's history.
        const newGroupIds = (
          await tx
            .select({ groupId: userGroupsTable.groupId })
            .from(userGroupsTable)
            .where(eq(userGroupsTable.userId, targetId))
        ).map((r) => r.groupId);
        await recordPermissionAudit(tx, {
          targetKind: "user",
          targetId,
          changeKind: "user.groups",
          actorUserId: req.session.userId ?? null,
          previousIds: [...newGroupIds, id],
          newIds: newGroupIds,
        });
      }
    }
    return del;
  });
  if (removed.length > 0) {
    if (kind === "users") {
      await emitAppsChangedToUsers([targetId]);
    } else {
      await emitAppsChangedToUsers(memberIdsBefore!);
    }
  }
  if (removed.length > 0) {
    const subject = kind === "users" ? "user" : kind === "apps" ? "app" : "role";
    // Best-effort: if the linked record still exists capture its name so the
    // log entry stays readable after a future delete. If it's already gone
    // we just record the id as before.
    const nameFields = (await loadSubResourceNameFields(kind, targetId)) ?? {};
    await db.insert(auditLogsTable).values({
      actorUserId: req.session.userId ?? null,
      action: `group.${subject}.remove`,
      targetType: "group",
      targetId: id,
      metadata: {
        groupName: group.name,
        [`${subject}Id`]: targetId,
        ...nameFields,
      },
    });
  }
  res.sendStatus(204);
});

router.delete("/groups/:id", requireAdmin, async (req, res): Promise<void> => {
  const id = Number(req.params.id);
  if (!Number.isInteger(id)) {
    res.status(400).json({ error: "Invalid id" });
    return;
  }
  const [existing] = await db.select().from(groupsTable).where(eq(groupsTable.id, id));
  if (!existing) {
    res.status(404).json({ error: "Group not found" });
    return;
  }
  if (existing.isSystem) {
    res.status(400).json({ error: "Cannot delete a system group" });
    return;
  }
  const force = req.query.force === "true" || req.query.force === "1";
  const memberIds = await getGroupMemberIds(id);
  const [appCountRow] = await db
    .select({ count: sql<number>`count(*)::int` })
    .from(groupAppsTable)
    .where(eq(groupAppsTable.groupId, id));
  const [roleCountRow] = await db
    .select({ count: sql<number>`count(*)::int` })
    .from(groupRolesTable)
    .where(eq(groupRolesTable.groupId, id));
  const memberCount = memberIds.length;
  const appCount = appCountRow?.count ?? 0;
  const roleCount = roleCountRow?.count ?? 0;
  const isNonEmpty = memberCount > 0 || appCount > 0 || roleCount > 0;
  if (isNonEmpty && !force) {
    res.status(409).json({
      error: "Group is not empty",
      memberCount,
      appCount,
      roleCount,
    });
    return;
  }
  await db.delete(groupsTable).where(eq(groupsTable.id, id));
  await db.insert(auditLogsTable).values({
    actorUserId: req.session.userId ?? null,
    action: "group.delete",
    targetType: "group",
    targetId: id,
    metadata: {
      name: existing.name,
      force,
      memberCount,
      appCount,
      roleCount,
    },
  });
  await emitAppsChangedToUsers(memberIds);
  res.sendStatus(204);
});

export default router;
