import { Router, type IRouter } from "express";
import { eq, and, or, ilike, inArray, asc, sql, type SQL } from "drizzle-orm";
import { db } from "@workspace/db";
import {
  usersTable,
  userRolesTable,
  rolesTable,
  groupsTable,
  userGroupsTable,
  notesTable,
  serviceOrdersTable,
  conversationsTable,
  messagesTable,
  auditLogsTable,
} from "@workspace/db";
import { requireAuth, requireAdmin, getUserRoles } from "../middlewares/auth";
import { emitAppsChangedToUsers } from "../lib/realtime";
import {
  listPermissionAudit,
  parsePermissionAuditFilters,
  recordPermissionAudit,
} from "../lib/permission-audit";
import {
  RegisterBody,
  CreateUserBody,
  GetUserParams,
  UpdateUserParams,
  UpdateUserBody,
  DeleteUserParams,
  AddUserRoleBody,
} from "@workspace/api-zod";
import bcrypt from "bcryptjs";

const router: IRouter = Router();

router.get("/users/directory", requireAuth, async (_req, res): Promise<void> => {
  const users = await db
    .select({
      id: usersTable.id,
      username: usersTable.username,
      displayNameAr: usersTable.displayNameAr,
      displayNameEn: usersTable.displayNameEn,
      avatarUrl: usersTable.avatarUrl,
      isActive: usersTable.isActive,
    })
    .from(usersTable)
    .where(eq(usersTable.isActive, true));
  res.json(users);
});

export type GroupSummary = {
  id: number;
  name: string;
  descriptionAr: string | null;
  descriptionEn: string | null;
};

export async function getGroupsForUser(userId: number): Promise<GroupSummary[]> {
  const rows = await db
    .select({
      id: groupsTable.id,
      name: groupsTable.name,
      descriptionAr: groupsTable.descriptionAr,
      descriptionEn: groupsTable.descriptionEn,
    })
    .from(userGroupsTable)
    .innerJoin(groupsTable, eq(userGroupsTable.groupId, groupsTable.id))
    .where(eq(userGroupsTable.userId, userId))
    .orderBy(groupsTable.name);
  return rows;
}

type DependencyCounts = {
  noteCount: number;
  orderCount: number;
  conversationCount: number;
  messageCount: number;
};

function buildUserProfile(
  user: typeof usersTable.$inferSelect,
  roles: string[],
  groups: GroupSummary[],
  counts?: DependencyCounts,
) {
  return {
    id: user.id,
    username: user.username,
    email: user.email,
    displayNameAr: user.displayNameAr,
    displayNameEn: user.displayNameEn,
    preferredLanguage: user.preferredLanguage,
    clockStyle: user.clockStyle,
    clockHour12: user.clockHour12,
    avatarUrl: user.avatarUrl,
    isActive: user.isActive,
    roles,
    groups,
    createdAt: user.createdAt,
    ...(counts ?? {}),
  };
}

router.post("/users", requireAdmin, async (req, res): Promise<void> => {
  const parsed = CreateUserBody.safeParse(req.body);
  if (!parsed.success) {
    res.status(400).json({ error: parsed.error.message });
    return;
  }

  const existing = await db
    .select({ id: usersTable.id })
    .from(usersTable)
    .where(eq(usersTable.username, parsed.data.username));

  if (existing.length > 0) {
    res.status(409).json({ error: "Username already taken" });
    return;
  }

  const requestedGroupIds = Array.isArray((parsed.data as { groupIds?: unknown }).groupIds)
    ? ((parsed.data as { groupIds?: number[] }).groupIds ?? []).filter(
        (id): id is number => Number.isInteger(id),
      )
    : [];

  if (requestedGroupIds.length > 0) {
    const found = await db
      .select({ id: groupsTable.id })
      .from(groupsTable)
      .where(inArray(groupsTable.id, requestedGroupIds));
    if (found.length !== new Set(requestedGroupIds).size) {
      res.status(400).json({ error: "One or more groupIds are invalid" });
      return;
    }
  }

  const passwordHash = await bcrypt.hash(parsed.data.password, 12);

  const user = await db.transaction(async (tx) => {
    const [created] = await tx
      .insert(usersTable)
      .values({
        username: parsed.data.username,
        email: parsed.data.email,
        passwordHash,
        displayNameAr: parsed.data.displayNameAr ?? null,
        displayNameEn: parsed.data.displayNameEn ?? null,
        preferredLanguage: parsed.data.preferredLanguage,
      })
      .returning();

    if (!created) throw new Error("Failed to create user");

    // Auto-assign Everyone group if it exists
    const [everyone] = await tx
      .select({ id: groupsTable.id })
      .from(groupsTable)
      .where(eq(groupsTable.name, "Everyone"));

    const groupIdsToAssign = new Set<number>(requestedGroupIds);
    if (everyone) groupIdsToAssign.add(everyone.id);

    if (groupIdsToAssign.size > 0) {
      await tx
        .insert(userGroupsTable)
        .values(
          Array.from(groupIdsToAssign).map((groupId) => ({
            userId: created.id,
            groupId,
          })),
        )
        .onConflictDoNothing();
    }

    return created;
  });

  const groups = await getGroupsForUser(user.id);
  res.status(201).json(buildUserProfile(user, [], groups));
});

router.get("/users", requireAdmin, async (req, res): Promise<void> => {
  const q = typeof req.query.q === "string" ? req.query.q.trim() : "";
  const groupIdRaw = req.query.groupId;
  const groupId =
    typeof groupIdRaw === "string" && /^\d+$/.test(groupIdRaw)
      ? Number(groupIdRaw)
      : undefined;
  const status = typeof req.query.status === "string" ? req.query.status : "";

  const conditions: Array<SQL> = [];
  if (q) {
    const pattern = `%${q}%`;
    const orExpr = or(
      ilike(usersTable.username, pattern),
      ilike(usersTable.email, pattern),
      ilike(usersTable.displayNameAr, pattern),
      ilike(usersTable.displayNameEn, pattern),
    );
    if (orExpr) conditions.push(orExpr);
  }
  if (status === "active") conditions.push(eq(usersTable.isActive, true));
  if (status === "disabled" || status === "inactive")
    conditions.push(eq(usersTable.isActive, false));

  if (groupId !== undefined) {
    const memberRows = await db
      .select({ userId: userGroupsTable.userId })
      .from(userGroupsTable)
      .where(eq(userGroupsTable.groupId, groupId));
    const userIdsForGroup = memberRows.map((r) => r.userId);
    if (userIdsForGroup.length === 0) {
      res.json([]);
      return;
    }
    conditions.push(inArray(usersTable.id, userIdsForGroup));
  }

  const whereClause =
    conditions.length === 0
      ? undefined
      : conditions.length === 1
        ? conditions[0]
        : and(...conditions);

  const users = whereClause
    ? await db.select().from(usersTable).where(whereClause).orderBy(asc(usersTable.createdAt))
    : await db.select().from(usersTable).orderBy(asc(usersTable.createdAt));

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

  const userIds = users.map((u) => u.id);

  // Batch load roles
  const roleRows = await db
    .select({ userId: userRolesTable.userId, roleName: rolesTable.name })
    .from(userRolesTable)
    .innerJoin(rolesTable, eq(userRolesTable.roleId, rolesTable.id))
    .where(inArray(userRolesTable.userId, userIds));
  const rolesByUser = new Map<number, string[]>();
  for (const r of roleRows) {
    const list = rolesByUser.get(r.userId) ?? [];
    list.push(r.roleName);
    rolesByUser.set(r.userId, list);
  }

  // Batch load groups
  const groupRows = await db
    .select({
      userId: userGroupsTable.userId,
      id: groupsTable.id,
      name: groupsTable.name,
      descriptionAr: groupsTable.descriptionAr,
      descriptionEn: groupsTable.descriptionEn,
    })
    .from(userGroupsTable)
    .innerJoin(groupsTable, eq(userGroupsTable.groupId, groupsTable.id))
    .where(inArray(userGroupsTable.userId, userIds))
    .orderBy(groupsTable.name);
  const groupsByUser = new Map<number, GroupSummary[]>();
  for (const g of groupRows) {
    const list = groupsByUser.get(g.userId) ?? [];
    list.push({
      id: g.id,
      name: g.name,
      descriptionAr: g.descriptionAr,
      descriptionEn: g.descriptionEn,
    });
    groupsByUser.set(g.userId, list);
  }

  // Batch load dependency counts so the admin delete dialog can warn on the
  // first click. Mirrors the pattern used by GET /groups. The lazy 409
  // fallback in DELETE /users/:id remains as a safety net.
  const noteRows = await db
    .select({
      userId: notesTable.userId,
      count: sql<number>`count(*)::int`,
    })
    .from(notesTable)
    .where(inArray(notesTable.userId, userIds))
    .groupBy(notesTable.userId);
  const orderRows = await db
    .select({
      userId: serviceOrdersTable.userId,
      count: sql<number>`count(*)::int`,
    })
    .from(serviceOrdersTable)
    .where(inArray(serviceOrdersTable.userId, userIds))
    .groupBy(serviceOrdersTable.userId);
  const convRows = await db
    .select({
      userId: conversationsTable.createdBy,
      count: sql<number>`count(*)::int`,
    })
    .from(conversationsTable)
    .where(inArray(conversationsTable.createdBy, userIds))
    .groupBy(conversationsTable.createdBy);
  const msgRows = await db
    .select({
      userId: messagesTable.senderId,
      count: sql<number>`count(*)::int`,
    })
    .from(messagesTable)
    .where(inArray(messagesTable.senderId, userIds))
    .groupBy(messagesTable.senderId);

  const noteMap = new Map(noteRows.map((r) => [r.userId, r.count]));
  const orderMap = new Map(orderRows.map((r) => [r.userId, r.count]));
  const convMap = new Map(convRows.map((r) => [r.userId, r.count]));
  const msgMap = new Map(msgRows.map((r) => [r.userId, r.count]));

  res.json(
    users.map((u) =>
      buildUserProfile(
        u,
        rolesByUser.get(u.id) ?? [],
        groupsByUser.get(u.id) ?? [],
        {
          noteCount: noteMap.get(u.id) ?? 0,
          orderCount: orderMap.get(u.id) ?? 0,
          conversationCount: convMap.get(u.id) ?? 0,
          messageCount: msgMap.get(u.id) ?? 0,
        },
      ),
    ),
  );
});

router.get(
  "/users/: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 [user] = await db
      .select({ id: usersTable.id })
      .from(usersTable)
      .where(eq(usersTable.id, id));
    if (!user) {
      res.status(404).json({ error: "User not found" });
      return;
    }
    const filters = parsePermissionAuditFilters(req, res);
    if (!filters) return;
    const result = await listPermissionAudit({
      targetKind: "user",
      targetId: id,
      limit,
      offset,
      filters,
    });
    res.json(result);
  },
);

router.get("/users/:id", requireAdmin, async (req, res): Promise<void> => {
  const params = GetUserParams.safeParse(req.params);
  if (!params.success) {
    res.status(400).json({ error: params.error.message });
    return;
  }

  const [user] = await db
    .select()
    .from(usersTable)
    .where(eq(usersTable.id, params.data.id));

  if (!user) {
    res.status(404).json({ error: "User not found" });
    return;
  }

  const roles = await getUserRoles(user.id);
  const groups = await getGroupsForUser(user.id);
  res.json(buildUserProfile(user, roles, groups));
});

router.patch("/users/:id", requireAdmin, async (req, res): Promise<void> => {
  const params = UpdateUserParams.safeParse(req.params);
  if (!params.success) {
    res.status(400).json({ error: params.error.message });
    return;
  }

  const parsed = UpdateUserBody.safeParse(req.body);
  if (!parsed.success) {
    res.status(400).json({ error: parsed.error.message });
    return;
  }

  const updateData: Partial<typeof usersTable.$inferInsert> = {};
  if (parsed.data.displayNameAr !== undefined) updateData.displayNameAr = parsed.data.displayNameAr;
  if (parsed.data.displayNameEn !== undefined) updateData.displayNameEn = parsed.data.displayNameEn;
  if (parsed.data.isActive !== undefined) updateData.isActive = parsed.data.isActive;
  if (parsed.data.preferredLanguage !== undefined) updateData.preferredLanguage = parsed.data.preferredLanguage;

  const userId = params.data.id;

  // Validate groupIds (if provided) BEFORE any destructive change to memberships.
  if (parsed.data.groupIds !== undefined && parsed.data.groupIds.length > 0) {
    const requested = parsed.data.groupIds;
    const found = await db
      .select({ id: groupsTable.id })
      .from(groupsTable)
      .where(inArray(groupsTable.id, requested));
    if (found.length !== new Set(requested).size) {
      res.status(400).json({ error: "One or more groupIds are invalid" });
      return;
    }
  }

  let user: typeof usersTable.$inferSelect | undefined;
  if (Object.keys(updateData).length > 0) {
    [user] = await db
      .update(usersTable)
      .set(updateData)
      .where(eq(usersTable.id, userId))
      .returning();
  } else {
    [user] = await db.select().from(usersTable).where(eq(usersTable.id, userId));
  }

  if (!user) {
    res.status(404).json({ error: "User not found" });
    return;
  }

  if (parsed.data.groupIds !== undefined) {
    // Same-transaction audit: read previous group ids, apply the change, and
    // write the permission_audit row atomically so the history can never
    // disagree with reality.
    await db.transaction(async (tx) => {
      const previousIds = (
        await tx
          .select({ groupId: userGroupsTable.groupId })
          .from(userGroupsTable)
          .where(eq(userGroupsTable.userId, userId))
      ).map((r) => r.groupId);
      await tx.delete(userGroupsTable).where(eq(userGroupsTable.userId, userId));
      if (parsed.data.groupIds && parsed.data.groupIds.length > 0) {
        await tx
          .insert(userGroupsTable)
          .values(parsed.data.groupIds.map((groupId) => ({ userId, groupId })))
          .onConflictDoNothing();
      }
      await recordPermissionAudit(tx, {
        targetKind: "user",
        targetId: userId,
        changeKind: "user.groups",
        actorUserId: req.session.userId ?? null,
        previousIds,
        newIds: parsed.data.groupIds ?? [],
      });
      // Mirror onto each affected group so its history shows the
      // membership change regardless of which editor was used.
      const newIds = parsed.data.groupIds ?? [];
      const added = newIds.filter((gid) => !previousIds.includes(gid));
      const removed = previousIds.filter((gid) => !newIds.includes(gid));
      for (const gid of [...added, ...removed]) {
        const newUserIds = (
          await tx
            .select({ userId: userGroupsTable.userId })
            .from(userGroupsTable)
            .where(eq(userGroupsTable.groupId, gid))
        ).map((r) => r.userId);
        const previousUserIds = added.includes(gid)
          ? newUserIds.filter((u) => u !== userId)
          : [...newUserIds, userId];
        await recordPermissionAudit(tx, {
          targetKind: "group",
          targetId: gid,
          changeKind: "group.users",
          actorUserId: req.session.userId ?? null,
          previousIds: previousUserIds,
          newIds: newUserIds,
        });
      }
    });
    await emitAppsChangedToUsers([userId]);
  }

  const roles = await getUserRoles(user.id);
  const groups = await getGroupsForUser(user.id);
  res.json(buildUserProfile(user, roles, groups));
});

router.post("/users/:id/roles", requireAdmin, async (req, res): Promise<void> => {
  const params = GetUserParams.safeParse(req.params);
  if (!params.success) {
    res.status(400).json({ error: params.error.message });
    return;
  }
  const parsed = AddUserRoleBody.safeParse(req.body);
  if (!parsed.success) {
    res.status(400).json({ error: parsed.error.message });
    return;
  }

  const [user] = await db
    .select()
    .from(usersTable)
    .where(eq(usersTable.id, params.data.id));
  if (!user) {
    res.status(404).json({ error: "User not found" });
    return;
  }

  const [role] = await db
    .select({ id: rolesTable.id })
    .from(rolesTable)
    .where(eq(rolesTable.name, parsed.data.roleName));
  if (!role) {
    res.status(404).json({ error: "Role not found" });
    return;
  }

  const inserted = await db.transaction(async (tx) => {
    const previousRoleIds = (
      await tx
        .select({ roleId: userRolesTable.roleId })
        .from(userRolesTable)
        .where(eq(userRolesTable.userId, user.id))
    ).map((r) => r.roleId);
    const ins = await tx
      .insert(userRolesTable)
      .values({ userId: user.id, roleId: role.id })
      .onConflictDoNothing()
      .returning();
    if (ins.length > 0) {
      await recordPermissionAudit(tx, {
        targetKind: "user",
        targetId: user.id,
        changeKind: "user.roles",
        actorUserId: req.session.userId ?? null,
        previousIds: previousRoleIds,
        newIds: [...previousRoleIds, role.id],
      });
    }
    return ins;
  });
  if (inserted.length > 0) {
    await emitAppsChangedToUsers([user.id]);
  }

  const roles = await getUserRoles(user.id);
  const groups = await getGroupsForUser(user.id);
  res.json(buildUserProfile(user, roles, groups));
});

router.delete(
  "/users/:id/roles/:roleName",
  requireAdmin,
  async (req, res): Promise<void> => {
    const params = GetUserParams.safeParse({ id: req.params.id });
    if (!params.success) {
      res.status(400).json({ error: params.error.message });
      return;
    }
    const roleName = String(req.params.roleName ?? "").trim();
    if (!roleName) {
      res.status(400).json({ error: "roleName required" });
      return;
    }

    const [user] = await db
      .select()
      .from(usersTable)
      .where(eq(usersTable.id, params.data.id));
    if (!user) {
      res.status(404).json({ error: "User not found" });
      return;
    }

    const [role] = await db
      .select({ id: rolesTable.id })
      .from(rolesTable)
      .where(eq(rolesTable.name, roleName));

    if (role) {
      const deleted = await db.transaction(async (tx) => {
        const previousRoleIds = (
          await tx
            .select({ roleId: userRolesTable.roleId })
            .from(userRolesTable)
            .where(eq(userRolesTable.userId, user.id))
        ).map((r) => r.roleId);
        const del = await tx
          .delete(userRolesTable)
          .where(
            and(
              eq(userRolesTable.userId, user.id),
              eq(userRolesTable.roleId, role.id),
            ),
          )
          .returning();
        if (del.length > 0) {
          await recordPermissionAudit(tx, {
            targetKind: "user",
            targetId: user.id,
            changeKind: "user.roles",
            actorUserId: req.session.userId ?? null,
            previousIds: previousRoleIds,
            newIds: previousRoleIds.filter((rid) => rid !== role.id),
          });
        }
        return del;
      });
      if (deleted.length > 0) {
        await emitAppsChangedToUsers([user.id]);
      }
    }

    const roles = await getUserRoles(user.id);
    const groups = await getGroupsForUser(user.id);
    res.json(buildUserProfile(user, roles, groups));
  },
);

router.delete("/users/:id", requireAdmin, async (req, res): Promise<void> => {
  const params = DeleteUserParams.safeParse(req.params);
  if (!params.success) {
    res.status(400).json({ error: params.error.message });
    return;
  }

  if (req.session.userId === params.data.id) {
    res.status(400).json({ error: "You cannot delete your own account" });
    return;
  }

  const userId = params.data.id;

  const [existing] = await db
    .select()
    .from(usersTable)
    .where(eq(usersTable.id, userId));
  if (!existing) {
    res.status(404).json({ error: "User not found" });
    return;
  }

  const force = req.query.force === "true" || req.query.force === "1";

  const [noteRow] = await db
    .select({ count: sql<number>`count(*)::int` })
    .from(notesTable)
    .where(eq(notesTable.userId, userId));
  const [orderRow] = await db
    .select({ count: sql<number>`count(*)::int` })
    .from(serviceOrdersTable)
    .where(eq(serviceOrdersTable.userId, userId));
  const [convRow] = await db
    .select({ count: sql<number>`count(*)::int` })
    .from(conversationsTable)
    .where(eq(conversationsTable.createdBy, userId));
  const [msgRow] = await db
    .select({ count: sql<number>`count(*)::int` })
    .from(messagesTable)
    .where(eq(messagesTable.senderId, userId));

  const noteCount = noteRow?.count ?? 0;
  const orderCount = orderRow?.count ?? 0;
  const conversationCount = convRow?.count ?? 0;
  const messageCount = msgRow?.count ?? 0;
  const hasDeps =
    noteCount > 0 ||
    orderCount > 0 ||
    conversationCount > 0 ||
    messageCount > 0;

  if (hasDeps && !force) {
    res.status(409).json({
      error: "User has dependent records",
      noteCount,
      orderCount,
      conversationCount,
      messageCount,
    });
    return;
  }

  await db.transaction(async (tx) => {
    if (force) {
      // Hard cleanup of records that don't cascade automatically so the
      // user delete can succeed without FK violations.
      if (conversationCount > 0) {
        await tx
          .delete(conversationsTable)
          .where(eq(conversationsTable.createdBy, userId));
      }
      if (messageCount > 0) {
        await tx
          .delete(messagesTable)
          .where(eq(messagesTable.senderId, userId));
      }
    }
    await tx.delete(usersTable).where(eq(usersTable.id, userId));
  });

  await db.insert(auditLogsTable).values({
    actorUserId: req.session.userId ?? null,
    action: "user.delete",
    targetType: "user",
    targetId: userId,
    metadata: {
      username: existing.username,
      email: existing.email,
      force,
      ...(hasDeps
        ? { noteCount, orderCount, conversationCount, messageCount }
        : {}),
    },
  });

  res.sendStatus(204);
});

export default router;
