import { db, account, role, user, userRole } from "@minmon/db";
import { hashCredentialPassword } from "@minmon/db/auth-utils";
import { asc, eq } from "drizzle-orm";

import { permissionedProcedure, protectedProcedure, router } from "../index";
import { byIdSchema, updateUserRolesSchema, userCreateSchema } from "./schemas";
import { badRequest, createEntityId, notFound } from "./utils";

async function getUsersWithRoles() {
  const [users, assignments] = await Promise.all([
    db
      .select({
        id: user.id,
        name: user.name,
        email: user.email,
        emailVerified: user.emailVerified,
        image: user.image,
        createdAt: user.createdAt,
        updatedAt: user.updatedAt,
      })
      .from(user)
      .orderBy(asc(user.name), asc(user.email)),
    db
      .select({
        userId: userRole.userId,
        roleId: role.id,
        roleName: role.name,
      })
      .from(userRole)
      .innerJoin(role, eq(role.id, userRole.roleId))
      .orderBy(asc(role.name)),
  ]);

  return users.map((item) => ({
    ...item,
    roles: assignments
      .filter((assignment) => assignment.userId === item.id)
      .map((assignment) => ({ id: assignment.roleId, name: assignment.roleName })),
  }));
}

async function validateRoleIds(roleIds: string[]) {
  const roles = await db.select({ id: role.id }).from(role);
  const validRoleIds = new Set(roles.map((item) => item.id));
  const uniqueRoleIds = Array.from(new Set(roleIds));

  const invalidRoleId = uniqueRoleIds.find((roleId) => !validRoleIds.has(roleId));
  if (invalidRoleId) {
    notFound(`Role not found: ${invalidRoleId}`);
  }

  return uniqueRoleIds;
}

export const usersRouter = router({
  me: protectedProcedure.query(({ ctx }) => ({
    user: ctx.session.user,
    roles: ctx.authorization.roles,
    permissions: ctx.authorization.permissions,
  })),

  list: permissionedProcedure("users.read").query(async () => getUsersWithRoles()),

  roles: permissionedProcedure("users.read").query(async () =>
    db
      .select({
        id: role.id,
        name: role.name,
        description: role.description,
      })
      .from(role)
      .orderBy(asc(role.name)),
  ),

  create: permissionedProcedure("users.create")
    .input(userCreateSchema)
    .mutation(async ({ input }) => {
      const [existingUser] = await db.select({ id: user.id }).from(user).where(eq(user.email, input.email)).limit(1);

      if (existingUser) {
        badRequest("A user with this email already exists");
      }

      const uniqueRoleIds = await validateRoleIds(input.roleIds);
      const userId = createEntityId();
      const now = new Date();
      const passwordHash = await hashCredentialPassword(input.password);

      await db.insert(user).values({
        id: userId,
        name: input.name,
        email: input.email,
        emailVerified: true,
        createdAt: now,
        updatedAt: now,
      });

      await db.insert(account).values({
        id: createEntityId(),
        accountId: userId,
        providerId: "credential",
        userId,
        password: passwordHash,
        createdAt: now,
        updatedAt: now,
      });

      await db.insert(userRole).values(
        uniqueRoleIds.map((roleId) => ({
          userId,
          roleId,
        })),
      );

      const users = await getUsersWithRoles();
      return users.find((item) => item.id === userId);
    }),

  updateRoles: permissionedProcedure("users.assignRoles")
    .input(updateUserRolesSchema)
    .mutation(async ({ input }) => {
      const [existingUser] = await db.select({ id: user.id }).from(user).where(eq(user.id, input.userId)).limit(1);

      if (!existingUser) {
        notFound("User not found");
      }

      const uniqueRoleIds = await validateRoleIds(input.roleIds);

      await db.delete(userRole).where(eq(userRole.userId, input.userId));
      await db.insert(userRole).values(
        uniqueRoleIds.map((roleId) => ({
          userId: input.userId,
          roleId,
        })),
      );

      const updated = await getUsersWithRoles();
      return updated.find((item) => item.id === input.userId);
    }),

  delete: permissionedProcedure("users.delete").input(byIdSchema).mutation(async ({ ctx, input }) => {
    if (ctx.session.user.id === input.id) {
      badRequest("You cannot delete your own user account while signed in");
    }

    const [existingUser] = await db.select({ id: user.id }).from(user).where(eq(user.id, input.id)).limit(1);

    if (!existingUser) {
      notFound("User not found");
    }

    await db.delete(user).where(eq(user.id, input.id));
    return { id: input.id };
  }),
});
