import { TRPCError } from "@trpc/server";
import bcrypt from "bcryptjs";
import { and, count, eq, ilike, inArray, or, type SQL } from "drizzle-orm";
import { z } from "zod";

import {
  adminProcedure,
  createTRPCRouter,
  protectedProcedure,
} from "~/server/api/trpc";
import {
  studyMembers,
  systemRoleEnum,
  users,
  userSystemRoles,
} from "~/server/db/schema";

export const usersRouter = createTRPCRouter({
  list: adminProcedure
    .input(
      z.object({
        page: z.number().min(1).default(1),
        limit: z.number().min(1).max(100).default(20),
        search: z.string().optional(),
        role: z.enum(systemRoleEnum.enumValues).optional(),
      }),
    )
    .query(async ({ ctx, input }) => {
      const { page, limit, search, role } = input;
      const offset = (page - 1) * limit;

      // Build where conditions
      const conditions: SQL[] = [];

      if (search) {
        conditions.push(
          or(
            ilike(users.name, `%${search}%`),
            ilike(users.email, `%${search}%`),
          )!,
        );
      }

      const whereClause =
        conditions.length > 0 ? and(...conditions) : undefined;

      // Get users with their roles
      const usersQuery = ctx.db.query.users.findMany({
        where: whereClause,
        with: {
          systemRoles: true,
        },
        columns: {
          password: false, // Exclude password
        },
        limit,
        offset,
        orderBy: (users, { asc }) => [asc(users.createdAt)],
      });

      // Get total count
      const countQuery = ctx.db
        .select({ count: count() })
        .from(users)
        .where(whereClause);

      const [usersList, totalCountResult] = await Promise.all([
        usersQuery,
        countQuery,
      ]);

      const totalCount = totalCountResult[0]?.count ?? 0;

      // Filter by role if specified
      let filteredUsers = usersList;
      if (role) {
        filteredUsers = usersList.filter((user) =>
          user.systemRoles.some((sr) => sr.role === role),
        );
      }

      return {
        users: filteredUsers.map((user) => ({
          ...user,
          roles: user.systemRoles.map((sr) => sr.role),
        })),
        pagination: {
          page,
          limit,
          total: totalCount,
          pages: Math.ceil(totalCount / limit),
        },
      };
    }),

  get: protectedProcedure
    .input(z.object({ id: z.string().uuid() }))
    .query(async ({ ctx, input }) => {
      const user = await ctx.db.query.users.findFirst({
        where: eq(users.id, input.id),
        with: {
          systemRoles: {
            with: {
              grantedByUser: {
                columns: {
                  id: true,
                  name: true,
                  email: true,
                },
              },
            },
          },
        },
        columns: {
          password: false, // Exclude password
        },
      });

      if (!user) {
        throw new TRPCError({
          code: "NOT_FOUND",
          message: "User not found",
        });
      }

      return {
        ...user,
        roles: user.systemRoles.map((sr) => ({
          role: sr.role,
          grantedAt: sr.grantedAt,
          grantedBy: sr.grantedByUser,
        })),
      };
    }),

  update: protectedProcedure
    .input(
      z.object({
        id: z.string().uuid(),
        name: z.string().min(1).max(255).optional(),
        email: z.string().email().optional(),
        image: z.string().url().optional(),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      const { id, ...updateData } = input;
      const currentUserId = ctx.session.user.id;

      // Check if user is updating their own profile or is an admin
      const isAdmin = await ctx.db.query.userSystemRoles.findFirst({
        where: and(
          eq(userSystemRoles.userId, currentUserId),
          eq(userSystemRoles.role, "administrator"),
        ),
      });

      if (id !== currentUserId && !isAdmin) {
        throw new TRPCError({
          code: "FORBIDDEN",
          message: "You can only update your own profile",
        });
      }

      // Check if user exists
      const existingUser = await ctx.db.query.users.findFirst({
        where: eq(users.id, id),
      });

      if (!existingUser) {
        throw new TRPCError({
          code: "NOT_FOUND",
          message: "User not found",
        });
      }

      // Check if email is already taken by another user
      if (updateData.email && updateData.email !== existingUser.email) {
        const emailExists = await ctx.db.query.users.findFirst({
          where: and(eq(users.email, updateData.email), eq(users.id, id)),
        });

        if (emailExists) {
          throw new TRPCError({
            code: "CONFLICT",
            message: "Email is already taken",
          });
        }
      }

      const [updatedUser] = await ctx.db
        .update(users)
        .set({
          ...updateData,
          updatedAt: new Date(),
        })
        .where(eq(users.id, id))
        .returning({
          id: users.id,
          name: users.name,
          email: users.email,
          image: users.image,
          updatedAt: users.updatedAt,
        });

      return updatedUser;
    }),

  changePassword: protectedProcedure
    .input(
      z.object({
        currentPassword: z.string().min(1, "Current password is required"),
        newPassword: z
          .string()
          .min(6, "Password must be at least 6 characters"),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      const { currentPassword, newPassword } = input;
      const userId = ctx.session.user.id;

      // Get current user with password
      const user = await ctx.db.query.users.findFirst({
        where: eq(users.id, userId),
      });

      if (!user?.password) {
        throw new TRPCError({
          code: "NOT_FOUND",
          message: "User not found",
        });
      }

      // Verify current password
      const isValidPassword = await bcrypt.compare(
        currentPassword,
        user.password,
      );
      if (!isValidPassword) {
        throw new TRPCError({
          code: "UNAUTHORIZED",
          message: "Current password is incorrect",
        });
      }

      // Hash new password
      const hashedNewPassword = await bcrypt.hash(newPassword, 12);

      // Update password
      await ctx.db
        .update(users)
        .set({
          password: hashedNewPassword,
          updatedAt: new Date(),
        })
        .where(eq(users.id, userId));

      return { success: true };
    }),

  assignRole: adminProcedure
    .input(
      z.object({
        userId: z.string().uuid(),
        role: z.enum(systemRoleEnum.enumValues),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      const { userId, role } = input;
      const currentUserId = ctx.session.user.id;

      // Check if target user exists
      const targetUser = await ctx.db.query.users.findFirst({
        where: eq(users.id, userId),
      });

      if (!targetUser) {
        throw new TRPCError({
          code: "NOT_FOUND",
          message: "User not found",
        });
      }

      // Check if role assignment already exists
      const existingRole = await ctx.db.query.userSystemRoles.findFirst({
        where: and(
          eq(userSystemRoles.userId, userId),
          eq(userSystemRoles.role, role),
        ),
      });

      if (existingRole) {
        throw new TRPCError({
          code: "CONFLICT",
          message: "User already has this role",
        });
      }

      // Assign the role
      const [newRole] = await ctx.db
        .insert(userSystemRoles)
        .values({
          userId,
          role,
          grantedBy: currentUserId,
        })
        .returning();

      return newRole;
    }),

  removeRole: adminProcedure
    .input(
      z.object({
        userId: z.string().uuid(),
        role: z.enum(systemRoleEnum.enumValues),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      const { userId, role } = input;

      // Check if role assignment exists
      const existingRole = await ctx.db.query.userSystemRoles.findFirst({
        where: and(
          eq(userSystemRoles.userId, userId),
          eq(userSystemRoles.role, role),
        ),
      });

      if (!existingRole) {
        throw new TRPCError({
          code: "NOT_FOUND",
          message: "User does not have this role",
        });
      }

      // Remove the role
      await ctx.db
        .delete(userSystemRoles)
        .where(
          and(
            eq(userSystemRoles.userId, userId),
            eq(userSystemRoles.role, role),
          ),
        );

      return { success: true };
    }),

  delete: adminProcedure
    .input(z.object({ id: z.string().uuid() }))
    .mutation(async ({ ctx, input }) => {
      const { id } = input;
      const currentUserId = ctx.session.user.id;

      // Prevent self-deletion
      if (id === currentUserId) {
        throw new TRPCError({
          code: "BAD_REQUEST",
          message: "You cannot delete your own account",
        });
      }

      // Check if user exists
      const existingUser = await ctx.db.query.users.findFirst({
        where: eq(users.id, id),
      });

      if (!existingUser) {
        throw new TRPCError({
          code: "NOT_FOUND",
          message: "User not found",
        });
      }

      // Soft delete the user
      await ctx.db
        .update(users)
        .set({
          deletedAt: new Date(),
          updatedAt: new Date(),
        })
        .where(eq(users.id, id));

      return { success: true };
    }),

  restore: adminProcedure
    .input(z.object({ id: z.string().uuid() }))
    .mutation(async ({ ctx, input }) => {
      const { id } = input;

      // Check if user exists and is deleted
      const existingUser = await ctx.db.query.users.findFirst({
        where: eq(users.id, id),
      });

      if (!existingUser) {
        throw new TRPCError({
          code: "NOT_FOUND",
          message: "User not found",
        });
      }

      if (!existingUser.deletedAt) {
        throw new TRPCError({
          code: "BAD_REQUEST",
          message: "User is not deleted",
        });
      }

      // Restore the user
      await ctx.db
        .update(users)
        .set({
          deletedAt: null,
          updatedAt: new Date(),
        })
        .where(eq(users.id, id));

      return { success: true };
    }),

  getWizards: protectedProcedure.query(async ({ ctx }) => {
    const userId = ctx.session.user.id;

    // Get all studies user is a member of
    const userStudies = await ctx.db.query.studyMembers.findMany({
      where: eq(studyMembers.userId, userId),
      columns: {
        studyId: true,
      },
    });

    const studyIds = userStudies.map((membership) => membership.studyId);

    if (studyIds.length === 0) {
      return [];
    }

    // Get all users who are members of the same studies and have wizard/researcher roles
    const studyMembersWithUsers = await ctx.db.query.studyMembers.findMany({
      where: inArray(studyMembers.studyId, studyIds),
      with: {
        user: {
          with: {
            systemRoles: true,
          },
          columns: {
            id: true,
            name: true,
            email: true,
          },
        },
      },
    });

    // Filter for users with wizard or researcher roles and deduplicate
    const wizardUsers = new Map();

    studyMembersWithUsers.forEach((member) => {
      const user = member.user;
      const hasWizardRole = user.systemRoles.some(
        (role) =>
          role.role === "wizard" ||
          role.role === "researcher" ||
          role.role === "administrator",
      );

      if (hasWizardRole && !wizardUsers.has(user.id)) {
        wizardUsers.set(user.id, {
          id: user.id,
          name: user.name,
          email: user.email,
          role:
            user.systemRoles.find(
              (role) =>
                role.role === "wizard" ||
                role.role === "researcher" ||
                role.role === "administrator",
            )?.role ?? "wizard",
        });
      }
    });

    return Array.from(wizardUsers.values()) as Array<{
      id: string;
      name: string;
      email: string;
      role: "wizard" | "researcher" | "administrator";
    }>;
  }),
});
