import { eq, inArray } from "drizzle-orm";
import { db } from "@workspace/db";
import {
  userRolesTable,
  groupRolesTable,
  userGroupsTable,
} from "@workspace/db";

export async function emitAppsChangedToUsers(userIds: number[]): Promise<void> {
  const unique = Array.from(new Set(userIds.filter((id) => Number.isInteger(id))));
  if (unique.length === 0) return;
  const { io } = await import("../index.js");
  for (const userId of unique) {
    io.to(`user:${userId}`).emit("apps_changed");
  }
}

async function getRoleHolderUserIds(roleId: number): Promise<number[]> {
  const directRows = await db
    .select({ userId: userRolesTable.userId })
    .from(userRolesTable)
    .where(eq(userRolesTable.roleId, roleId));

  const groupIdRows = await db
    .select({ groupId: groupRolesTable.groupId })
    .from(groupRolesTable)
    .where(eq(groupRolesTable.roleId, roleId));
  const groupIds = groupIdRows.map((r) => r.groupId);

  const indirectRows = groupIds.length > 0
    ? await db
        .select({ userId: userGroupsTable.userId })
        .from(userGroupsTable)
        .where(inArray(userGroupsTable.groupId, groupIds))
    : [];

  return [
    ...directRows.map((r) => r.userId),
    ...indirectRows.map((r) => r.userId),
  ];
}

/**
 * Resolve every user that currently carries `roleId`, either directly via
 * `user_roles` or indirectly via `group_roles` -> `user_groups`, and emit
 * `apps_changed` to each of them.
 */
export async function emitAppsChangedToRoleHolders(roleId: number): Promise<void> {
  if (!Number.isInteger(roleId)) return;
  const userIds = await getRoleHolderUserIds(roleId);
  await emitAppsChangedToUsers(userIds);
}

/**
 * Broadcast that an executive-meetings day has changed (a meeting was
 * created/updated/deleted/duplicated, attendees changed, or a cell-merge
 * was set/cleared). Listeners on the executive-meetings page invalidate
 * their cached query for the affected day so all viewers stay in sync
 * without a manual refresh. The payload is just the ISO date string —
 * no PII — and clients without executive access simply ignore it.
 */
export async function emitExecutiveMeetingsDayChanged(
  date: string | null | undefined,
): Promise<void> {
  if (typeof date !== "string" || date.length === 0) return;
  const { io } = await import("../index.js");
  io.emit("executive_meetings_changed", { date });
}

/**
 * Convenience wrapper that fans out `emitExecutiveMeetingsDayChanged` over
 * a set of dates, deduplicated and with empty/null entries dropped. Used by
 * mutations that can affect more than one day at once — most notably a
 * meeting reschedule (PATCH `meetingDate`) or an approved reschedule
 * request, where viewers on both the old and the new day need to refetch.
 */
export async function emitExecutiveMeetingsDaysChanged(
  dates: ReadonlyArray<string | null | undefined>,
): Promise<void> {
  const unique = Array.from(
    new Set(
      dates.filter(
        (d): d is string => typeof d === "string" && d.length > 0,
      ),
    ),
  );
  if (unique.length === 0) return;
  for (const d of unique) {
    await emitExecutiveMeetingsDayChanged(d);
  }
}

/**
 * Notify every holder of `roleId` that the role's permission set has been
 * updated, so the client can invalidate cached `/api/auth/me` and
 * role/permission queries and re-evaluate permission-driven UI without a
 * page refresh.
 */
export async function emitRolePermissionsChangedToHolders(
  roleId: number,
): Promise<void> {
  if (!Number.isInteger(roleId)) return;
  const userIds = await getRoleHolderUserIds(roleId);
  const unique = Array.from(new Set(userIds.filter((id) => Number.isInteger(id))));
  if (unique.length === 0) return;
  const { io } = await import("../index.js");
  for (const userId of unique) {
    io.to(`user:${userId}`).emit("role_permissions_changed", { roleId });
  }
}
