import { eq, inArray } from "drizzle-orm";
import { db } from "@workspace/db";
import {
  userRolesTable,
  groupRolesTable,
  userGroupsTable,
  rolePermissionsTable,
} 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);
  }
}

/**
 * #277: Notify a single user that their per-user alert state for a meeting
 * has changed (Done / Dismiss). The alert state is private to each user —
 * Ahmed clicking "Done" must NOT clear Sara's alert — so this event is
 * emitted ONLY to the acting user's `user:${userId}` room. All of that
 * user's open tabs / devices then invalidate their alert-state query and
 * the alert disappears within ~1s instead of waiting for the 30s poll.
 *
 * Meeting mutations (postpone / reschedule / cancel) are deliberately NOT
 * routed through this event — those modify the meeting itself and continue
 * to broadcast globally via `executive_meetings_changed`.
 */
export async function emitExecutiveMeetingAlertStateChanged(
  userId: number,
  payload: {
    meetingId: number;
    dismissed: boolean;
    acknowledged: boolean;
  },
): Promise<void> {
  if (!Number.isInteger(userId) || userId <= 0) return;
  if (!Number.isInteger(payload.meetingId) || payload.meetingId <= 0) return;
  const { io } = await import("../index.js");
  io.to(`user:${userId}`).emit(
    "executive_meeting_alert_state_changed",
    payload,
  );
}

/**
 * #216: Resolve every user that currently holds `permissionId` (via any role
 * they hold directly through `user_roles` or transitively through
 * `group_roles` -> `user_groups`) and emit `apps_changed` to each of them.
 *
 * Used by app-permission CRUD: when an app's required-permission set
 * changes, the visibility of that app for any holder of the affected
 * permission may have changed (gained or lost), so they need to refetch
 * `/api/apps`. Users who do not hold the permission cannot be affected by
 * this change in isolation, so we deliberately skip them — admins always
 * see all apps and refetch on the response of their own write.
 */
export async function emitAppsChangedToPermissionHolders(
  permissionId: number,
): Promise<void> {
  if (!Number.isInteger(permissionId)) return;
  const roleIdRows = await db
    .select({ roleId: rolePermissionsTable.roleId })
    .from(rolePermissionsTable)
    .where(eq(rolePermissionsTable.permissionId, permissionId));
  const roleIds = Array.from(new Set(roleIdRows.map((r) => r.roleId)));
  if (roleIds.length === 0) return;

  const directRows = await db
    .select({ userId: userRolesTable.userId })
    .from(userRolesTable)
    .where(inArray(userRolesTable.roleId, roleIds));

  const groupIdRows = await db
    .select({ groupId: groupRolesTable.groupId })
    .from(groupRolesTable)
    .where(inArray(groupRolesTable.roleId, roleIds));
  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))
    : [];

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

/**
 * 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 });
  }
}
