/* eslint-disable @typescript-eslint/no-explicit-any -- Convex table-generic helpers; runtime validators enforce the shapes */
import { v } from "convex/values";
import { action, internalMutation, internalQuery, mutation, query, type QueryCtx, type MutationCtx } from "./_generated/server";
import { internal } from "./_generated/api";
import type { Id } from "./_generated/dataModel";
import { createAccount, modifyAccountCredentials, retrieveAccount, getAuthSessionId } from "@convex-dev/auth/server";
import { assertPermission, actorLite, AppError, requireActor, can, currentActor, revokeUserSessions } from "./lib/access";
import { audit } from "./lib/audit";
import { notify } from "./lib/notify";
import { nextCode } from "./lib/seq";
import { getSettings } from "./lib/settings";
import { todayKey } from "./lib/days";
import { PERMISSION_KEYS, PERMISSIONS } from "../src/lib/permissions";
import { USER_STATUSES } from "../src/lib/domain";
import { normalizePhone } from "../src/lib/format";

const workerArgs = {
  fullName: v.string(),
  email: v.string(),
  username: v.optional(v.union(v.string(), v.null())),
  phone: v.optional(v.union(v.string(), v.null())),
  emergencyContact: v.optional(v.union(v.string(), v.null())),
  roleId: v.id("roles"),
  hireDate: v.optional(v.union(v.string(), v.null())),
  locale: v.optional(v.string()),
};

function validate(d: { fullName: string; email: string; username?: string | null }) {
  const fields: Record<string, string> = {};
  if (d.fullName.trim().length < 2) fields.fullName = "Name is required";
  if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(d.email.trim())) fields.email = "Invalid email";
  if (d.username && !/^[a-z0-9._-]{3,30}$/i.test(d.username)) fields.username = "Letters, digits, dots, dashes";
  if (Object.keys(fields).length) throw new AppError(Object.values(fields)[0], "VALIDATION", { fields });
}

// ── Internal helpers used by the account-provisioning actions ─
export const _prepareCreate = internalMutation({
  args: { ...workerArgs, actorId: v.id("users") },
  returns: v.object({ id: v.id("users"), email: v.string() }),
  handler: async (ctx, { actorId, ...data }) => {
    validate(data);
    const email = data.email.toLowerCase().trim();
    if (await ctx.db.query("users").withIndex("email", (q) => q.eq("email", email)).unique()) throw new AppError("This email is already used.", "VALIDATION", { fields: { email: "Already used" } });
    const username = data.username ? data.username.toLowerCase() : undefined;
    if (username && (await ctx.db.query("users").withIndex("by_username", (q) => q.eq("username", username)).unique())) throw new AppError("This username is already used.", "VALIDATION", { fields: { username: "Already used" } });
    const role = await ctx.db.get(data.roleId);
    if (!role) throw new AppError("Role not found", "NOT_FOUND");
    const actor = await ctx.db.get(actorId);
    const actorRole = actor?.roleId ? await ctx.db.get(actor.roleId) : null;
    if (role.key === "ADMIN" && actorRole?.key !== "ADMIN") throw new AppError("Only an admin can create admin accounts.", "PERMISSION");
    const code = await nextCode(ctx, "worker");
    const id = await ctx.db.insert("users", { code, email, name: data.fullName.trim(), fullName: data.fullName.trim(), username, phone: data.phone ? normalizePhone(data.phone) : undefined, emergencyContact: data.emergencyContact || undefined, roleId: role._id, status: "ACTIVE", hireDate: data.hireDate || undefined, locale: data.locale ?? "en", timezone: "Africa/Casablanca", twoFactorEnabled: false, permissionOverrides: [], passwordChangedAt: Date.now() });
    await audit(ctx, actor ? { id: actor._id, fullName: actor.fullName ?? "", roleKey: actorRole?.key ?? "", sessionId: null } : null, { action: "WORKER_CREATED", module: "workers", entityType: "user", entityId: id, entityLabel: data.fullName, newValue: { email, role: role.name, code } });
    return { id, email };
  },
});

export const _rollbackCreate = internalMutation({
  args: { id: v.id("users") },
  returns: v.null(),
  handler: async (ctx, { id }) => {
    await ctx.db.delete(id);
    return null;
  },
});

export const _actorForAction = internalQuery({
  args: {},
  returns: v.union(v.null(), v.object({ id: v.id("users"), fullName: v.string(), roleKey: v.string(), isAdmin: v.boolean(), perms: v.array(v.string()), email: v.string() })),
  handler: async (ctx) => {
    const a = await currentActor(ctx);
    return a ? { id: a.id, fullName: a.fullName, roleKey: a.roleKey, isAdmin: a.isAdmin, perms: [...a.perms], email: a.user.email ?? "" } : null;
  },
});

/** Create a worker and its password account (accounts are only created by administrators). */
export const create = action({
  args: { ...workerArgs, password: v.string() },
  returns: v.object({ id: v.id("users") }),
  handler: async (ctx, { password, ...data }): Promise<{ id: Id<"users"> }> => {
    const actor = await ctx.runQuery(internal.workers._actorForAction, {});
    if (!actor || !(actor.isAdmin || actor.perms.includes("workers.manage"))) throw new AppError("You don't have permission to do this.", "PERMISSION");
    if (password.length < 8) throw new AppError("Password must be at least 8 characters.", "VALIDATION", { fields: { password: "At least 8 characters" } });
    const { id, email } = await ctx.runMutation(internal.workers._prepareCreate, { ...data, actorId: actor.id });
    try {
      await createAccount(ctx, { provider: "password", account: { id: email, secret: password }, profile: { email } });
    } catch (e) {
      await ctx.runMutation(internal.workers._rollbackCreate, { id });
      throw e;
    }
    return { id };
  },
});

export const update = mutation({
  args: { id: v.id("users"), ...workerArgs },
  returns: v.null(),
  handler: async (ctx, { id, ...data }) => {
    const user = await assertPermission(ctx, "workers.manage");
    validate(data);
    const w = await ctx.db.get(id);
    if (!w || w.deletedAt) throw new AppError("Worker not found", "NOT_FOUND");
    const email = data.email.toLowerCase().trim();
    if (email !== w.email && (await ctx.db.query("users").withIndex("email", (q) => q.eq("email", email)).unique())) throw new AppError("This email is already used.", "VALIDATION");
    const username = data.username ? data.username.toLowerCase() : undefined;
    if (username && username !== w.username && (await ctx.db.query("users").withIndex("by_username", (q) => q.eq("username", username)).unique())) throw new AppError("This username is already used.", "VALIDATION");
    const role = await ctx.db.get(data.roleId);
    if (!role) throw new AppError("Role not found", "NOT_FOUND");
    const prevRole = w.roleId ? await ctx.db.get(w.roleId) : null;
    if (role.key === "ADMIN" && !user.isAdmin) throw new AppError("Only an admin can grant the admin role.", "PERMISSION");
    if (prevRole?.key === "ADMIN" && role.key !== "ADMIN") {
      const admins = (await ctx.db.query("users").withIndex("by_role", (q) => q.eq("roleId", prevRole._id)).collect()).filter((u) => u.status === "ACTIVE" && !u.deletedAt);
      if (admins.length <= 1) throw new AppError("At least one active admin is required.", "VALIDATION");
    }
    if (email !== w.email) {
      // Keep the password account id in sync with the login email.
      const account = (await ctx.db.query("authAccounts").withIndex("userIdAndProvider", (q) => q.eq("userId", id).eq("provider", "password")).unique()) as { _id: Id<"authAccounts"> } | null;
      if (account) await ctx.db.patch(account._id, { providerAccountId: email });
    }
    await ctx.db.patch(id, { email, name: data.fullName.trim(), fullName: data.fullName.trim(), username, phone: data.phone ? normalizePhone(data.phone) : undefined, emergencyContact: data.emergencyContact || undefined, roleId: role._id, hireDate: data.hireDate || undefined, locale: data.locale ?? w.locale ?? "en" });
    await audit(ctx, actorLite(user), { action: "WORKER_EDITED", module: "workers", entityType: "user", entityId: id, entityLabel: data.fullName, previousValue: { email: w.email, role: prevRole?.name ?? null }, newValue: { email, role: role.name } });
    if (prevRole && prevRole._id !== role._id) await notify(ctx, { type: "SECURITY_ALERT", title: "Role changed", body: `${user.fullName} changed ${data.fullName}'s role from ${prevRole.name} to ${role.name}.`, href: `/workers/${id}`, actorId: user.id, priority: "HIGH" });
    return null;
  },
});

/** Admin password reset for a worker. Signs the worker out of every device. */
export const resetPassword = action({
  args: { id: v.id("users"), password: v.string() },
  returns: v.null(),
  handler: async (ctx, { id, password }) => {
    const actor = await ctx.runQuery(internal.workers._actorForAction, {});
    if (!actor || !(actor.isAdmin || actor.perms.includes("workers.manage"))) throw new AppError("You don't have permission to do this.", "PERMISSION");
    if (password.length < 8) throw new AppError("Password must be at least 8 characters.", "VALIDATION");
    const target = await ctx.runQuery(internal.workers._email, { id });
    if (!target) throw new AppError("Worker not found", "NOT_FOUND");
    await modifyAccountCredentials(ctx, { provider: "password", account: { id: target, secret: password } });
    await ctx.runMutation(internal.workers._afterPasswordReset, { id, actorId: actor.id });
    return null;
  },
});

export const _email = internalQuery({ args: { id: v.id("users") }, returns: v.union(v.string(), v.null()), handler: async (ctx, { id }) => (await ctx.db.get(id))?.email ?? null });

export const _afterPasswordReset = internalMutation({
  args: { id: v.id("users"), actorId: v.id("users") },
  returns: v.null(),
  handler: async (ctx, { id, actorId }) => {
    const [w, actor] = await Promise.all([ctx.db.get(id), ctx.db.get(actorId)]);
    const role = actor?.roleId ? await ctx.db.get(actor.roleId) : null;
    await ctx.db.patch(id, { passwordChangedAt: Date.now() });
    await revokeUserSessions(ctx, id);
    await audit(ctx, actor ? { id: actor._id, fullName: actor.fullName ?? "", roleKey: role?.key ?? "", sessionId: null } : null, { action: "PASSWORD_CHANGED", module: "auth", entityType: "user", entityId: id, entityLabel: w?.fullName ?? "", newValue: { byAdmin: true }, severity: "WARNING" });
    return null;
  },
});

export const setStatus = mutation({
  args: { id: v.id("users"), status: v.string(), reason: v.string() },
  returns: v.null(),
  handler: async (ctx, { id, status, reason }) => {
    const user = await assertPermission(ctx, "workers.manage");
    if (!USER_STATUSES.includes(status as (typeof USER_STATUSES)[number])) throw new AppError("Invalid status", "VALIDATION");
    const w = await ctx.db.get(id);
    if (!w) throw new AppError("Worker not found", "NOT_FOUND");
    if (w._id === user.id) throw new AppError("You cannot change your own status.", "VALIDATION");
    const role = w.roleId ? await ctx.db.get(w.roleId) : null;
    if (role?.key === "ADMIN" && status !== "ACTIVE") {
      const admins = (await ctx.db.query("users").withIndex("by_role", (q) => q.eq("roleId", role._id)).collect()).filter((u) => u.status === "ACTIVE" && !u.deletedAt);
      if (admins.length <= 1) throw new AppError("At least one active admin is required.", "VALIDATION");
    }
    await ctx.db.patch(id, { status });
    if (status !== "ACTIVE") await revokeUserSessions(ctx, id);
    await audit(ctx, actorLite(user), { action: status === "ACTIVE" ? "WORKER_STATUS_CHANGED" : "ACCOUNT_DISABLED", module: "workers", entityType: "user", entityId: id, entityLabel: w.fullName ?? "", previousValue: w.status, newValue: status, reason, severity: status === "ACTIVE" ? "INFO" : "WARNING" });
    return null;
  },
});

export const remove = mutation({
  args: { id: v.id("users"), reason: v.string() },
  returns: v.null(),
  handler: async (ctx, { id, reason }) => {
    const user = await assertPermission(ctx, "workers.manage");
    if (!user.isAdmin) throw new AppError("Only an admin can delete accounts.", "PERMISSION");
    const w = await ctx.db.get(id);
    if (!w) throw new AppError("Worker not found", "NOT_FOUND");
    if (w._id === user.id) throw new AppError("You cannot delete your own account.", "VALIDATION");
    const role = w.roleId ? await ctx.db.get(w.roleId) : null;
    if (role?.key === "ADMIN") throw new AppError("Admin accounts cannot be deleted. Deactivate instead.", "VALIDATION");
    await ctx.db.patch(id, { deletedAt: Date.now(), status: "INACTIVE", email: `deleted-${Date.now()}-${w.email}`, username: undefined });
    for (const acc of await ctx.db.query("authAccounts").withIndex("userIdAndProvider", (q) => q.eq("userId", id)).collect()) await ctx.db.delete(acc._id);
    await revokeUserSessions(ctx, id);
    await audit(ctx, actorLite(user), { action: "WORKER_STATUS_CHANGED", module: "workers", entityType: "user", entityId: id, entityLabel: w.fullName ?? "", previousValue: w.status, newValue: "DELETED", reason, severity: "WARNING" });
    return null;
  },
});

/** Per-user overrides: true grants, false revokes, null = inherit from the role. */
export const setPermissionOverrides = mutation({
  args: { userId: v.id("users"), overrides: v.any(), reason: v.optional(v.string()) },
  returns: v.null(),
  handler: async (ctx, { userId, overrides, reason }) => {
    const user = await assertPermission(ctx, "workers.permissions");
    const w = await ctx.db.get(userId);
    if (!w) throw new AppError("Worker not found", "NOT_FOUND");
    const role = w.roleId ? await ctx.db.get(w.roleId) : null;
    if (role?.key === "ADMIN") throw new AppError("Admin permissions cannot be restricted.", "VALIDATION");
    const prev = Object.fromEntries((w.permissionOverrides ?? []).map((o) => [o.key, o.granted]));
    const next: Record<string, boolean> = { ...prev };
    for (const [key, val] of Object.entries(overrides as Record<string, boolean | null>)) {
      if (!PERMISSION_KEYS.includes(key)) continue;
      if (val === null || val === undefined) delete next[key];
      else next[key] = val;
    }
    await ctx.db.patch(userId, { permissionOverrides: Object.entries(next).map(([key, granted]) => ({ key, granted })) });
    await audit(ctx, actorLite(user), { action: "PERMISSIONS_CHANGED", module: "workers", entityType: "user", entityId: userId, entityLabel: w.fullName ?? "", previousValue: prev, newValue: overrides, reason, severity: "WARNING" });
    await notify(ctx, { type: "SECURITY_ALERT", title: "Permissions changed", body: `${user.fullName} changed the permissions of ${w.fullName}.`, href: `/workers/${userId}`, actorId: user.id, priority: "HIGH" });
    return null;
  },
});

export const saveRole = mutation({
  args: { id: v.optional(v.id("roles")), key: v.optional(v.string()), name: v.string(), description: v.optional(v.union(v.string(), v.null())), permissions: v.array(v.string()) },
  returns: v.object({ id: v.id("roles") }),
  handler: async (ctx, input) => {
    const user = await assertPermission(ctx, "workers.permissions");
    const name = input.name.trim();
    if (name.length < 2) throw new AppError("Role name is required", "VALIDATION");
    const permissions = input.permissions.filter((k) => PERMISSION_KEYS.includes(k));
    let id = input.id;
    if (id) {
      const role = await ctx.db.get(id);
      if (!role) throw new AppError("Role not found", "NOT_FOUND");
      if (role.key === "ADMIN") throw new AppError("The admin role always has every permission.", "VALIDATION");
      await ctx.db.patch(id, { name, description: input.description || undefined, permissions });
    } else {
      const key = (input.key ?? name).toUpperCase().replace(/[^A-Z0-9]+/g, "_");
      if (await ctx.db.query("roles").withIndex("by_key", (q) => q.eq("key", key)).unique()) throw new AppError("A role with this key already exists.", "VALIDATION");
      id = await ctx.db.insert("roles", { key, name, description: input.description || undefined, isSystem: false, permissions });
    }
    await audit(ctx, actorLite(user), { action: input.id ? "ROLE_EDITED" : "ROLE_CREATED", module: "workers", entityType: "role", entityId: id, entityLabel: name, newValue: { permissions }, severity: "WARNING" });
    return { id };
  },
});

// ── Self-service ─────────────────────────────────────────────
export const changeOwnPassword = action({
  args: { current: v.string(), next: v.string() },
  returns: v.null(),
  handler: async (ctx, { current, next }) => {
    const actor = await ctx.runQuery(internal.workers._actorForAction, {});
    if (!actor) throw new AppError("Your session has expired. Please sign in again.", "AUTH");
    const settings = await ctx.runQuery(internal.workers._settingsLite, {});
    if (next.length < settings.passwordMinLength) throw new AppError(`Password must be at least ${settings.passwordMinLength} characters.`, "VALIDATION");
    try {
      await retrieveAccount(ctx, { provider: "password", account: { id: actor.email, secret: current } });
    } catch {
      throw new AppError("Current password is incorrect.", "VALIDATION", { fields: { current: "Incorrect" } });
    }
    await modifyAccountCredentials(ctx, { provider: "password", account: { id: actor.email, secret: next } });
    await ctx.runMutation(internal.workers._afterOwnPasswordChange, {});
    return null;
  },
});

export const _settingsLite = internalQuery({ args: {}, returns: v.object({ passwordMinLength: v.number() }), handler: async (ctx) => ({ passwordMinLength: (await getSettings(ctx)).passwordMinLength }) });

export const _afterOwnPasswordChange = internalMutation({
  args: {},
  returns: v.null(),
  handler: async (ctx) => {
    const actor = await requireActor(ctx);
    await ctx.db.patch(actor.id, { passwordChangedAt: Date.now() });
    const sessionId = await getAuthSessionId(ctx);
    await revokeUserSessions(ctx, actor.id, sessionId ? [sessionId] : []);
    await audit(ctx, actorLite(actor), { action: "PASSWORD_CHANGED", module: "auth", entityType: "user", entityId: actor.id, entityLabel: actor.fullName });
    return null;
  },
});

export const updateOwnProfile = mutation({
  args: { fullName: v.string(), phone: v.optional(v.union(v.string(), v.null())), locale: v.optional(v.string()) },
  returns: v.null(),
  handler: async (ctx, { fullName, phone, locale }) => {
    const actor = await requireActor(ctx);
    const name = fullName.trim();
    if (name.length < 2) throw new AppError("Name is required", "VALIDATION");
    await ctx.db.patch(actor.id, { fullName: name, name, phone: phone ? normalizePhone(phone) : undefined, locale: locale ?? actor.user.locale });
    await audit(ctx, actorLite(actor), { action: "WORKER_EDITED", module: "workers", entityType: "user", entityId: actor.id, entityLabel: name, newValue: { self: true } });
    return null;
  },
});

// ── Queries ──────────────────────────────────────────────────
export const roles = query({
  args: {},
  returns: v.array(v.object({ id: v.id("roles"), key: v.string(), name: v.string(), description: v.union(v.string(), v.null()), isSystem: v.boolean(), permissions: v.array(v.string()), members: v.number() })),
  handler: async (ctx) => {
    await requireActor(ctx);
    const roles = await ctx.db.query("roles").collect();
    return Promise.all(roles.map(async (r) => ({ id: r._id, key: r.key, name: r.name, description: r.description ?? null, isSystem: r.isSystem, permissions: r.key === "ADMIN" ? PERMISSIONS.map((p) => p.key) : r.permissions, members: (await ctx.db.query("users").withIndex("by_role", (q) => q.eq("roleId", r._id)).collect()).filter((u) => !u.deletedAt).length })));
  },
});

export const list = query({
  args: {},
  returns: v.array(v.any()),
  handler: async (ctx) => {
    await assertPermission(ctx, "workers.view");
    const settings = await getSettings(ctx);
    const monthStartMs = Date.parse(`${todayKey(settings.timezone).slice(0, 7)}-01T00:00:00Z`);
    const roles = await ctx.db.query("roles").collect();
    const users = (await ctx.db.query("users").collect()).filter((u) => u.roleId && !u.deletedAt).sort((a, b) => (a.status ?? "").localeCompare(b.status ?? "") || (a.fullName ?? "").localeCompare(b.fullName ?? ""));
    const stays = [...(await ctx.db.query("reservations").withIndex("by_status_checkIn", (q) => q.eq("status", "CHECKED_IN")).collect()), ...(await ctx.db.query("reservations").withIndex("by_status_checkIn", (q) => q.eq("status", "CHECKED_OUT")).collect())];
    return Promise.all(
      users.map(async (u) => {
        const role = roles.find((r) => r._id === u.roleId);
        const created = await ctx.db.query("reservations").withIndex("by_createdBy_createdAt", (q) => q.eq("createdById", u._id)).collect();
        const commissions = await ctx.db.query("commissions").withIndex("by_worker", (q) => q.eq("workerId", u._id)).collect();
        return { id: u._id, code: u.code ?? null, fullName: u.fullName ?? "", email: u.email ?? "", phone: u.phone ?? null, avatarUrl: null, role: role?.name ?? "", roleKey: role?.key ?? "", status: u.status ?? "ACTIVE", hireDate: u.hireDate ?? null, lastLoginAt: u.lastLoginAt ?? null, lastSeenAt: u.lastSeenAt ?? null, online: !!u.lastSeenAt && Date.now() - u.lastSeenAt < 3 * 60_000, total: created.length, confirmed: created.filter((r) => ["CONFIRMED", "CHECKED_IN", "CHECKED_OUT"].includes(r.status)).length, cancelled: created.filter((r) => r.status === "CANCELLED" || r.status === "NO_SHOW").length, thisMonth: created.filter((r) => r.createdAt >= monthStartMs).length, checkIns: stays.filter((r) => r.checkedInById === u._id).length, checkOuts: stays.filter((r) => r.checkedOutById === u._id).length, commissionEarned: commissions.filter((c) => !["CANCELLED", "REVERSED"].includes(c.status)).reduce((s, c) => s + c.amount, 0), commissionPaid: commissions.filter((c) => c.status === "PAID").reduce((s, c) => s + c.amount, 0), commissionPending: commissions.filter((c) => c.status === "PENDING" || c.status === "APPROVED").reduce((s, c) => s + c.amount, 0) };
      })
    );
  },
});

export const get = query({
  args: { id: v.id("users") },
  returns: v.union(v.null(), v.any()),
  handler: async (ctx, { id }) => {
    const actor = await assertPermission(ctx, "workers.view");
    const w = await ctx.db.get(id);
    if (!w || w.deletedAt) return null;
    const role = w.roleId ? await ctx.db.get(w.roleId) : null;
    const [created, commissions, tasks, activity, loginHistory, actionCounts] = await Promise.all([
      ctx.db.query("reservations").withIndex("by_createdBy_createdAt", (q) => q.eq("createdById", id)).order("desc").take(300),
      ctx.db.query("commissions").withIndex("by_worker", (q) => q.eq("workerId", id)).order("desc").take(300),
      Promise.all(["TODO", "IN_PROGRESS", "COMPLETED", "CANCELLED"].map((s) => ctx.db.query("tasks").withIndex("by_assignee_status", (q) => q.eq("assigneeId", id).eq("status", s)).take(50))).then((x) => x.flat()),
      ctx.db.query("auditLog").withIndex("by_user_at", (q) => q.eq("userId", id)).order("desc").take(80),
      ctx.db.query("loginHistory").withIndex("by_user_at", (q) => q.eq("userId", id)).order("desc").take(40),
      ctx.db.query("auditLog").withIndex("by_user_at", (q) => q.eq("userId", id)).take(4000),
    ]);
    const counts: Record<string, number> = {};
    for (const a of actionCounts) counts[a.action] = (counts[a.action] ?? 0) + 1;
    const createdRows = await Promise.all(created.map(async (r) => ({ id: r._id, code: r.code, status: r.status, source: r.source, checkIn: r.checkIn, checkOut: r.checkOut, totalAmount: r.totalAmount, createdAt: r.createdAt, customer: (await ctx.db.get(r.customerId))?.fullName ?? "", apartment: (await ctx.db.get(r.apartmentId))?.code ?? "" })));
    const commissionRows = await Promise.all(commissions.map(async (c) => { const r = await ctx.db.get(c.reservationId); const [cu, a, ap] = await Promise.all([r ? ctx.db.get(r.customerId) : null, r ? ctx.db.get(r.apartmentId) : null, c.approvedById ? ctx.db.get(c.approvedById) : null]); return { id: c._id, code: c.code, amount: c.amount, status: c.status, triggerEvent: c.triggerEvent, createdAt: c.createdAt, approvedAt: c.approvedAt ?? null, paidAt: c.paidAt ?? null, adminNotes: c.adminNotes ?? null, reservation: r ? { id: r._id, code: r.code, checkIn: r.checkIn, status: r.status, customer: cu?.fullName ?? "", apartment: a?.code ?? "" } : null, approvedBy: ap?.fullName ?? null }; }));
    const taskRows = await Promise.all(tasks.map(async (t) => ({ id: t._id, title: t.title, status: t.status, priority: t.priority, dueDate: t.dueDate ?? null, apartment: t.apartmentId ? (await ctx.db.get(t.apartmentId))?.code ?? null : null, createdAt: t.createdAt })));
    const checkIns = (await ctx.db.query("reservations").withIndex("by_status_checkIn", (q) => q.eq("status", "CHECKED_IN")).collect()).filter((r) => r.checkedInById === id).length + (await ctx.db.query("reservations").withIndex("by_status_checkIn", (q) => q.eq("status", "CHECKED_OUT")).collect()).filter((r) => r.checkedInById === id).length;
    const checkOuts = (await ctx.db.query("reservations").withIndex("by_status_checkIn", (q) => q.eq("status", "CHECKED_OUT")).collect()).filter((r) => r.checkedOutById === id).length;
    const sessions = can(actor, "workers.activity") || can(actor, "security.view") ? await sessionsOf(ctx, id) : [];
    return { ...w, id: w._id, role: role ? { id: role._id, key: role.key, name: role.name, description: role.description ?? null, permissions: role.permissions } : null, created: createdRows, commissions: commissionRows, tasks: taskRows, activity: activity.map((a) => ({ ...a, id: a._id })), loginHistory: loginHistory.map((l) => ({ ...l, id: l._id })), actionCounts: counts, checkIns, checkOuts, sessions, failedLogins7d: loginHistory.filter((l) => !l.success && l.at >= Date.now() - 7 * 86_400_000).length };
  },
});

export async function sessionsOf(ctx: QueryCtx | MutationCtx, userId: Id<"users">) {
  const sessions = (await ctx.db.query("authSessions").withIndex("userId", (q) => q.eq("userId", userId)).collect()) as { _id: Id<"authSessions">; _creationTime: number; expirationTime: number }[];
  const metas = await ctx.db.query("sessionMeta").withIndex("by_user", (q) => q.eq("userId", userId)).collect();
  const me = await getAuthSessionId(ctx);
  return sessions
    .filter((s) => s.expirationTime > Date.now())
    .map((s) => {
      const m = metas.find((x) => x.sessionId === s._id);
      return { id: s._id, device: m?.device ?? null, browser: m?.browser ?? null, ipAddress: m?.ipAddress ?? null, createdAt: s._creationTime, lastSeenAt: m?.lastSeenAt ?? s._creationTime, revokedAt: null as number | null, current: s._id === me };
    })
    .sort((a, b) => b.lastSeenAt - a.lastSeenAt);
}
