import { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository";
import { getTranslation } from "@calcom/i18n/server";
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
import { buildNonDelegationCredentials } from "@calcom/lib/delegationCredential";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { withSelectedCalendars } from "@calcom/lib/server/withSelectedCalendars";
import type { PrismaClient } from "@calcom/prisma";
import { availabilityUserSelect } from "@calcom/prisma";
import type { DestinationCalendar, SelectedCalendar, User as UserType } from "@calcom/prisma/client";
import { Prisma } from "@calcom/prisma/client";
import type { IdentityProvider } from "@calcom/prisma/enums";
import type { CreationSource } from "@calcom/prisma/enums";
import { BookingStatus, MembershipRole } from "@calcom/prisma/enums";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import { userSelect as prismaUserSelect } from "@calcom/prisma/selects/user";
import { userMetadata } from "@calcom/prisma/zod-utils";
import type { UpId, UserProfile } from "@calcom/types/UserProfile";
import type { z } from "zod";

const whereClauseForOrgWithSlugOrRequestedSlug = (..._args: unknown[]) => ({});
const getParsedTeam = <T>(team: T): T => team;

export type { UserWithLegacySelectedCalendars } from "@calcom/lib/server/withSelectedCalendars";
export { withSelectedCalendars };
export type UserAdminTeams = number[];

export type SessionUser = {
  id: number;
  username: string | null;
  name: string | null;
  email: string;
  emailVerified: Date | null;
  bio: string | null;
  avatarUrl: string | null;
  timeZone: string;
  weekStart: string;
  startTime: number;
  endTime: number;
  defaultScheduleId: number | null;
  bufferTime: number;
  theme: string | null;
  appTheme: string | null;
  createdDate: Date;
  hideBranding: boolean;
  twoFactorEnabled: boolean;
  identityProvider: string | null;
  identityProviderId: string | null;
  brandColor: string | null;
  darkBrandColor: string | null;
  movedToProfileId: number | null;
  completedOnboarding: boolean;
  destinationCalendar: DestinationCalendar | null;
  locale: string;
  timeFormat: number | null;
  trialEndsAt: Date | null;
  metadata: z.infer<typeof userMetadata>;
  role: string;
  allowDynamicBooking: boolean;
  allowSEOIndexing: boolean;
  receiveMonthlyDigestEmail: boolean;
  requiresBookerEmailVerification: boolean;
  profiles: UserProfile[];
  allSelectedCalendars: SelectedCalendar[];
  userLevelSelectedCalendars: SelectedCalendar[];
};

const log = logger.getSubLogger({ prefix: ["[repository/user]"] });

export const ORGANIZATION_ID_UNKNOWN = "ORGANIZATION_ID_UNKNOWN";

const teamSelect = {
  id: true,
  name: true,
  slug: true,
  metadata: true,
  logoUrl: true,
  organizationSettings: true,
  isOrganization: true,
  isPlatform: true,
} satisfies Prisma.TeamSelect;

const userSelect = {
  id: true,
  uuid: true,
  username: true,
  name: true,
  email: true,
  emailVerified: true,
  bio: true,
  avatarUrl: true,
  timeZone: true,
  weekStart: true,
  bufferTime: true,
  hideBranding: true,
  theme: true,
  createdDate: true,
  trialEndsAt: true,
  completedOnboarding: true,
  locale: true,
  timeFormat: true,
  twoFactorSecret: true,
  twoFactorEnabled: true,
  backupCodes: true,
  identityProviderId: true,
  invitedTo: true,
  brandColor: true,
  darkBrandColor: true,
  allowDynamicBooking: true,
  allowSEOIndexing: true,
  receiveMonthlyDigestEmail: true,
  requiresBookerEmailVerification: true,
  verified: true,
  locked: true,
  movedToProfileId: true,
  metadata: true,
  isPlatformManaged: true,
  lastActiveAt: true,
  identityProvider: true,
  teams: true,
  profiles: true,
} satisfies Prisma.UserSelect;

export class UserRepository {
  constructor(private prismaClient: PrismaClient) {}

  async findTeamsByUserId({ userId }: { userId: UserType["id"] }) {
    const teamMemberships = await this.prismaClient.membership.findMany({
      where: {
        userId: userId,
      },
      include: {
        team: {
          select: teamSelect,
        },
      },
    });

    const acceptedTeamMemberships = teamMemberships.filter((membership) => membership.accepted);
    const pendingTeamMemberships = teamMemberships.filter((membership) => !membership.accepted);

    return {
      teams: acceptedTeamMemberships.map((membership) => membership.team),
      memberships: teamMemberships,
      acceptedTeamMemberships,
      pendingTeamMemberships,
    };
  }

  async findOrganizations({ userId }: { userId: UserType["id"] }) {
    const { acceptedTeamMemberships } = await this.findTeamsByUserId({
      userId,
    });

    const acceptedOrgMemberships = acceptedTeamMemberships.filter(
      (membership) => membership.team.isOrganization
    );

    const organizations = acceptedOrgMemberships.map((membership) => membership.team);

    return {
      organizations,
    };
  }

  /**
   * It is aware of the fact that a user can be part of multiple organizations.
   */
  async findUsersByUsername({ orgSlug, usernameList }: { orgSlug: string | null; usernameList: string[] }) {
    const { where, profiles } = await this._getWhereClauseForFindingUsersByUsername({
      orgSlug,
      usernameList,
    });

    return (
      await this.prismaClient.user.findMany({
        select: userSelect,
        where,
      })
    ).map((user) => {
      // User isn't part of any organization
      if (!profiles) {
        return {
          ...user,
          profile: ProfileRepository.buildPersonalProfileFromUser({ user }),
        };
      }
      const profile = profiles.find((profile) => profile.user.id === user.id) ?? null;
      if (!profile) {
        log.error("Profile not found for user", safeStringify({ user, profiles }));
        // Profile must be there because profile itself was used to retrieve the user
        throw new Error("Profile couldn't be found");
      }
      const { user: _1, ...profileWithoutUser } = profile;
      return {
        ...user,
        profile: profileWithoutUser,
      };
    });
  }

  async findPlatformMembersByUsernames({ usernameList }: { usernameList: string[] }) {
    return (
      await this.prismaClient.user.findMany({
        select: userSelect,
        where: {
          username: {
            in: usernameList,
          },
          isPlatformManaged: false,
          profiles: {
            some: {
              organization: {
                isPlatform: true,
              },
            },
          },
        },
      })
    ).map((user) => {
      return {
        ...user,
        profile: ProfileRepository.buildPersonalProfileFromUser({ user }),
      };
    });
  }

  async _getWhereClauseForFindingUsersByUsername({
    orgSlug,
    usernameList,
  }: {
    orgSlug: string | null;
    usernameList: string[];
  }) {
    // Lookup in profiles because that's where the organization usernames exist
    const profiles = orgSlug
      ? (
          await ProfileRepository.findManyByOrgSlugOrRequestedSlug({
            orgSlug: orgSlug,
            usernames: usernameList,
          })
        ).map((profile) => ({
          ...profile,
          organization: getParsedTeam(profile.organization),
        }))
      : null;
    const where =
      profiles && profiles.length > 0
        ? {
            // Get UserIds from profiles
            id: {
              in: profiles.map((profile) => profile.user.id),
            },
          }
        : {
            username: {
              in: usernameList,
            },
            ...(orgSlug
              ? {
                  organization: whereClauseForOrgWithSlugOrRequestedSlug(orgSlug),
                }
              : {
                  organization: null,
                }),
          };
    return { where, profiles };
  }

  async findByEmail({ email }: { email: string }) {
    const user = await this.prismaClient.user.findUnique({
      where: {
        email: email.toLowerCase(),
      },
      select: userSelect,
    });
    return user;
  }

  async findManyByEmailsWithEmailVerificationSettings({ emails }: { emails: string[] }) {
    const normalizedEmails = emails.map((e) => e.toLowerCase());

    if (!normalizedEmails.length) return [];
    const users = await this.findVerifiedUsersByEmailsRaw(normalizedEmails);

    if (!users.length) return [];
    return users.map((u) => ({
      email: u.email,
      matchedEmail: u.matchedEmail,
      requiresBookerEmailVerification: u.requiresBookerEmailVerification,
    }));
  }

  private async findVerifiedUsersByEmailsRaw(emails: string[]) {
    const emailListSql = Prisma.join(emails.map((e) => Prisma.sql`${e}`));
    return this.prismaClient.$queryRaw<
      Array<{
        id: number;
        email: string;
        matchedEmail: string;
        requiresBookerEmailVerification: boolean;
      }>
    >(Prisma.sql`
      SELECT
        u."id",
        u."email",
        u."email" AS "matchedEmail",
        u."requiresBookerEmailVerification"
      FROM
        "public"."users" AS u
      WHERE
        u."email" IN (${emailListSql})
        AND u."emailVerified" IS NOT NULL
        AND u."locked" = FALSE
      UNION
      SELECT
        u."id",
        u."email",
        t0."email" AS "matchedEmail",
        u."requiresBookerEmailVerification"
      FROM
        "public"."users" AS u
      INNER JOIN "public"."SecondaryEmail" AS t0
        ON t0."userId" = u."id"
      WHERE
        t0."email" IN (${emailListSql})
        AND t0."emailVerified" IS NOT NULL
        AND u."locked" = FALSE
    `);
  }

  async findByEmailAndIncludeProfilesAndPassword({ email }: { email: string }) {
    const user = await this.prismaClient.user.findUnique({
      where: {
        email: email.toLowerCase(),
      },
      select: {
        locked: true,
        role: true,
        id: true,
        uuid: true,
        username: true,
        name: true,
        email: true,
        metadata: true,
        identityProvider: true,
        password: true,
        twoFactorEnabled: true,
        twoFactorSecret: true,
        backupCodes: true,
        locale: true,
        teams: {
          include: {
            team: {
              select: teamSelect,
            },
          },
        },
        createdDate: true,
      },
    });

    if (!user) {
      return null;
    }

    const allProfiles = await ProfileRepository.findAllProfilesForUserIncludingMovedUser(user);
    return {
      ...user,
      allProfiles,
    };
  }

  async findById({ id }: { id: number }) {
    const user = await this.prismaClient.user.findUnique({
      where: {
        id,
      },
      select: userSelect,
    });

    if (!user) {
      return null;
    }
    return {
      ...user,
      metadata: userMetadata.parse(user.metadata),
    };
  }

  async findSecondaryEmailByUserIdAndEmail({ userId, email }: { userId: number; email: string }) {
    return this.prismaClient.secondaryEmail.findUnique({
      where: {
        userId_email: {
          userId,
          email,
        },
      },
      select: {
        id: true,
        emailVerified: true,
      },
    });
  }

  async findByUuid({ uuid }: { uuid: string }) {
    return this.prismaClient.user.findUnique({
      where: {
        uuid,
      },
      select: {
        name: true,
        email: true,
        avatarUrl: true,
      },
    });
  }

  async findByIds({ ids }: { ids: number[] }) {
    return this.prismaClient.user.findMany({
      where: {
        id: {
          in: ids,
        },
      },
      select: userSelect,
    });
  }

  async findByIdsWithPagination({
    ids,
    search,
    cursor,
    limit,
  }: {
    ids: number[];
    search?: string | null;
    cursor?: number | null;
    limit?: number | null;
  }) {
    const where: Record<string, unknown> = {
      id: cursor ? { in: ids, gt: cursor } : { in: ids },
    };

    if (search) {
      where.OR = [
        { name: { contains: search, mode: "insensitive" } },
        { email: { contains: search, mode: "insensitive" } },
      ];
    }

    const users = await this.prismaClient.user.findMany({
      where,
      select: {
        id: true,
        name: true,
        email: true,
      },
      orderBy: { id: "asc" },
      ...(limit ? { take: limit + 1 } : {}),
    });

    if (!limit) {
      return { users, nextCursor: undefined, total: users.length };
    }

    const hasMore = users.length > limit;
    const items = hasMore ? users.slice(0, limit) : users;
    const nextCursor = hasMore ? items[items.length - 1].id : undefined;

    // Only count on the first page to avoid an extra query on every scroll
    let total: number | undefined;
    if (!cursor) {
      const countWhere: Record<string, unknown> = {
        id: { in: ids },
      };
      if (search) {
        countWhere.OR = [
          { name: { contains: search, mode: "insensitive" } },
          { email: { contains: search, mode: "insensitive" } },
        ];
      }
      total = await this.prismaClient.user.count({ where: countWhere });
    }

    return { users: items, nextCursor, total };
  }

  async findByUuids({ uuids }: { uuids: string[] }) {
    if (uuids.length === 0) return [];
    return this.prismaClient.user.findMany({
      where: {
        uuid: {
          in: uuids,
        },
      },
      select: {
        id: true,
        uuid: true,
        name: true,
        email: true,
        avatarUrl: true,
      },
    });
  }

  async findByIdOrThrow({ id }: { id: number }) {
    const user = await this.findById({ id });
    if (!user) {
      throw new Error(`User with id ${id} not found`);
    }
    return user;
  }

  async findManyByOrganization({ organizationId }: { organizationId: number }) {
    const profiles = await ProfileRepository.findManyForOrg({ organizationId });
    return profiles.map((profile) => profile.user);
  }

  isAMemberOfOrganization({
    user,
    organizationId,
  }: {
    user: { profiles: { organizationId: number }[] };
    organizationId: number;
  }) {
    return user.profiles.some((profile) => profile.organizationId === organizationId);
  }

  async findIfAMemberOfSomeOrganization({ user }: { user: { id: number } }) {
    return !!(
      await ProfileRepository.findManyForUser({
        id: user.id,
      })
    ).length;
  }

  isMigratedToOrganization({
    user,
  }: {
    user: {
      metadata?: {
        migratedToOrgFrom?: unknown;
      } | null;
    };
  }) {
    return !!user.metadata?.migratedToOrgFrom;
  }

  async isMovedToAProfile({ user }: { user: Pick<UserType, "movedToProfileId"> }) {
    return !!user.movedToProfileId;
  }

  async swapPrimaryEmailWithSecondaryEmail({
    userId,
    secondaryEmailId,
    oldPrimaryEmail,
    oldPrimaryEmailVerified,
    newPrimaryEmail,
    userUpdateData,
  }: {
    userId: number;
    secondaryEmailId: number;
    oldPrimaryEmail: string;
    oldPrimaryEmailVerified: Date | null;
    newPrimaryEmail: string;
    userUpdateData?: Prisma.UserUpdateInput;
  }) {
    const [, updatedUser] = await this.prismaClient.$transaction([
      this.prismaClient.secondaryEmail.update({
        where: {
          id: secondaryEmailId,
          userId: userId,
        },
        data: {
          email: oldPrimaryEmail,
          emailVerified: oldPrimaryEmailVerified,
        },
      }),
      this.prismaClient.user.update({
        where: { id: userId },
        data: {
          ...userUpdateData,
          email: newPrimaryEmail,
        },
      }),
    ]);

    return updatedUser;
  }

  async enrichUserWithTheProfile<T extends { username: string | null; id: number }>({
    user,
    upId,
  }: {
    user: T;
    upId: UpId;
  }) {
    const profile = await ProfileRepository.findByUpIdWithAuth(upId, user.id);
    if (!profile) {
      return {
        ...user,
        profile: ProfileRepository.buildPersonalProfileFromUser({ user }),
      };
    }
    return {
      ...user,
      profile,
    };
  }

  /**
   * Use this method instead of `enrichUserWithTheProfile` if you don't directly have the profileId.
   * It can happen in following cases:
   * 1. While dealing with a User that hasn't been added to any organization yet and thus have no Profile entries.
   * 2. While dealing with a User that has been moved to a Profile i.e. he was invited to an organization when he was an existing user.
   * 3. We haven't added profileId to all the entities, so they aren't aware of which profile they belong to. So, we still mostly use this function to enrich the user with its profile.
   */
  async enrichUserWithItsProfile<
    T extends {
      id: number;
      username: string | null;
    },
  >({
    user,
  }: {
    user: T;
  }): Promise<
    T & {
      nonProfileUsername: string | null;
      profile: UserProfile;
    }
  > {
    const profiles = await ProfileRepository.findManyForUser({ id: user.id });
    if (profiles.length) {
      const profile = profiles[0];
      // platform org user doesn't need org profile
      if (profile?.organization?.isPlatform) {
        return {
          ...user,
          nonProfileUsername: user.username,
          profile: ProfileRepository.buildPersonalProfileFromUser({ user }),
        };
      }

      return {
        ...user,
        username: profile.username,
        nonProfileUsername: user.username,
        profile,
      };
    }

    // If no organization profile exists, use the personal profile so that the returned user is normalized to have a profile always
    return {
      ...user,
      nonProfileUsername: user.username,
      profile: ProfileRepository.buildPersonalProfileFromUser({ user }),
    };
  }

  async enrichUserWithItsProfileExcludingOrgMetadata<
    T extends {
      id: number;
      username: string | null;
    },
  >({
    user,
  }: {
    user: T;
  }): Promise<
    T & {
      nonProfileUsername: string | null;
      profile: UserProfile;
    }
  > {
    const enrichedUser = await this.enrichUserWithItsProfile({ user });

    const { profile } = enrichedUser;

    if (!profile || !profile.organization) {
      return enrichedUser;
    }

    // Exclude organization metadata
    const sanitizedProfile: UserProfile = {
      ...profile,
      organization: {
        ...profile.organization,
        metadata: {},
        organizationSettings: profile.organization.organizationSettings,
      },
    };

    return {
      ...enrichedUser,
      profile: sanitizedProfile,
    };
  }

  async enrichUsersWithTheirProfiles<T extends { id: number; username: string | null }>(
    users: T[]
  ): Promise<
    Array<
      T & {
        nonProfileUsername: string | null;
        profile: UserProfile;
      }
    >
  > {
    if (users.length === 0) return [];

    const userIds = users.map((user) => user.id);
    const profiles = await ProfileRepository.findManyForUsers(userIds);

    // Create a Map for faster lookups, preserving arrays of profiles per user
    const profileMap = new Map<number, UserProfile[]>();
    profiles.forEach((profile) => {
      if (!profileMap.has(profile.userId)) {
        profileMap.set(profile.userId, []);
      }
      profileMap.get(profile.userId)?.push(profile);
    });

    // Precompute personal profiles for all users
    const personalProfileMap = new Map<number, UserProfile>();
    users.forEach((user) => {
      personalProfileMap.set(user.id, ProfileRepository.buildPersonalProfileFromUser({ user }));
    });

    return users.map((user) => {
      const userProfiles = profileMap.get(user.id) || [];
      if (userProfiles.length > 0) {
        const profile = userProfiles[0];
        if (profile?.organization?.isPlatform) {
          return {
            ...user,
            nonProfileUsername: user.username,
            profile: personalProfileMap.get(user.id)!,
          };
        }

        return {
          ...user,
          username: profile.username,
          nonProfileUsername: user.username,
          profile,
        };
      }

      // If no organization profile exists, use the precomputed personal profile
      return {
        ...user,
        nonProfileUsername: user.username,
        profile: personalProfileMap.get(user.id)!,
      };
    });
  }

  async enrichUsersWithTheirProfileExcludingOrgMetadata<T extends { id: number; username: string | null }>(
    users: T[]
  ): Promise<
    Array<
      T & {
        nonProfileUsername: string | null;
        profile: UserProfile;
      }
    >
  > {
    const enrichedUsers = await this.enrichUsersWithTheirProfiles(users);

    return enrichedUsers.map((enrichedUser) => {
      const { profile } = enrichedUser;

      if (!profile || !profile.organization) {
        return enrichedUser;
      }

      // Exclude organization metadata
      const sanitizedProfile: UserProfile = {
        ...profile,
        organization: {
          ...profile.organization,
          metadata: {},
          organizationSettings: profile.organization.organizationSettings,
        },
      };

      return {
        ...enrichedUser,
        profile: sanitizedProfile,
      };
    });
  }

  enrichUserWithItsProfileBuiltFromUser<T extends { id: number; username: string | null }>({
    user,
  }: {
    user: T;
  }): T & {
    nonProfileUsername: string | null;
    profile: UserProfile;
  } {
    // If no organization profile exists, use the personal profile so that the returned user is normalized to have a profile always
    return {
      ...user,
      nonProfileUsername: user.username,
      profile: ProfileRepository.buildPersonalProfileFromUser({ user }),
    };
  }

  async enrichEntityWithProfile<
    T extends
      | {
          profile: {
            id: number;
            username: string | null;
            organizationId: number | null;
            organization?: {
              id: number;
              name: string;
              calVideoLogo?: string | null;
              bannerUrl: string | null;
              slug: string | null;
              metadata: Prisma.JsonValue;
            };
          };
        }
      | {
          user: {
            username: string | null;
            id: number;
          };
        },
  >(entity: T) {
    if ("profile" in entity) {
      const { profile, ...entityWithoutProfile } = entity;
      const { organization, ...profileWithoutOrganization } = profile || {};
      const parsedOrg = organization ? getParsedTeam(organization) : null;

      const ret = {
        ...entityWithoutProfile,
        profile: {
          ...profileWithoutOrganization,
          ...(parsedOrg
            ? {
                organization: parsedOrg,
              }
            : {
                organization: null,
              }),
        },
      };
      return ret;
    } else {
      const profiles = await ProfileRepository.findManyForUser(entity.user);
      if (!profiles.length) {
        return {
          ...entity,
          profile: ProfileRepository.buildPersonalProfileFromUser({
            user: entity.user,
          }),
        };
      } else {
        return {
          ...entity,
          profile: profiles[0],
        };
      }
    }
  }

  async updateWhereId({
    whereId,
    data,
  }: {
    whereId: number;
    data: {
      movedToProfileId?: number | null;
    };
  }) {
    return this.prismaClient.user.update({
      where: {
        id: whereId,
      },
      data: {
        movedToProfile: data.movedToProfileId
          ? {
              connect: {
                id: data.movedToProfileId,
              },
            }
          : undefined,
      },
    });
  }

  async create(
    data: Omit<Prisma.UserCreateInput, "password" | "organization" | "movedToProfile"> & {
      username: string | null;
      hashedPassword?: string;
      organizationId: number | null;
      creationSource: CreationSource;
      locked: boolean;
    }
  ) {
    const { email, username, creationSource, locked, hashedPassword, organizationId, ...rest } = data;

    log.info("create user", {
      email,
      username,
      organizationId,
      locked,
    });
    const t = await getTranslation("en", "common");
    const availability = getAvailabilityFromSchedule(DEFAULT_SCHEDULE);

    const user = await this.prismaClient.user.create({
      data: {
        username,
        email: email,
        ...(hashedPassword && {
          password: { create: { hash: hashedPassword } },
        }),
        // Default schedule
        schedules: {
          create: {
            name: t("default_schedule_name"),
            availability: {
              createMany: {
                data: availability.map((schedule) => ({
                  days: schedule.days,
                  startTime: schedule.startTime,
                  endTime: schedule.endTime,
                })),
              },
            },
          },
        },
        creationSource,
        locked,
        ...(organizationId && username
          ? {
              organizationId,
              profiles: {
                create: {
                  username,
                  organizationId,
                  uid: ProfileRepository.generateProfileUid(),
                },
              },
            }
          : {}),
        ...rest,
      },
    });

    return user;
  }
  async getUserAdminTeams({ userId }: { userId: number }) {
    return await this.prismaClient.user.findUnique({
      where: {
        id: userId,
      },
      select: {
        id: true,
        avatarUrl: true,
        name: true,
        username: true,
        teams: {
          where: {
            accepted: true,
            OR: [
              {
                role: { in: [MembershipRole.ADMIN, MembershipRole.OWNER] },
              },
              {
                team: {
                  parent: {
                    members: {
                      some: {
                        id: userId,
                        role: {
                          in: [MembershipRole.ADMIN, MembershipRole.OWNER],
                        },
                      },
                    },
                  },
                },
              },
            ],
          },
          select: {
            team: {
              select: {
                id: true,
                name: true,
                logoUrl: true,
                isOrganization: true,
                parent: {
                  select: {
                    logoUrl: true,
                    name: true,
                    id: true,
                  },
                },
              },
            },
          },
        },
      },
    });
  }
  async isAdminOfTeamOrParentOrg({ userId, teamId }: { userId: number; teamId: number }) {
    const membershipQuery = {
      members: {
        some: {
          userId,
          role: { in: [MembershipRole.ADMIN, MembershipRole.OWNER] },
        },
      },
    };
    const teams = await this.prismaClient.team.findMany({
      where: {
        id: teamId,
        OR: [
          membershipQuery,
          {
            parent: { ...membershipQuery },
          },
        ],
      },
      select: {
        id: true,
      },
    });
    return !!teams.length;
  }
  async isAdminOrOwnerOfTeam({ userId, teamId }: { userId: number; teamId: number }) {
    const isAdminOrOwnerOfTeam = await this.prismaClient.membership.findUnique({
      where: {
        userId_teamId: {
          userId,
          teamId,
        },
        role: { in: [MembershipRole.ADMIN, MembershipRole.OWNER] },
        accepted: true,
      },
      select: {
        id: true,
      },
    });
    return !!isAdminOrOwnerOfTeam;
  }

  async getUserOrganizationAndTeams({ userId }: { userId: number }) {
    return await this.prismaClient.user.findUnique({
      where: { id: userId },
      select: {
        organizationId: true,
        teams: {
          where: { accepted: true },
          select: { teamId: true },
        },
      },
    });
  }
  async getTimeZoneAndDefaultScheduleId({ userId }: { userId: number }) {
    return await this.prismaClient.user.findUnique({
      where: {
        id: userId,
      },
      select: {
        timeZone: true,
        defaultScheduleId: true,
      },
    });
  }

  async adminFindById(userId: number) {
    return await this.prismaClient.user.findUniqueOrThrow({
      where: {
        id: userId,
      },
    });
  }

  async findUserTeams({ id }: { id: number }) {
    const user = await this.prismaClient.user.findUnique({
      where: {
        id,
      },
      select: {
        completedOnboarding: true,
        teams: {
          select: {
            accepted: true,
            team: {
              select: {
                id: true,
                name: true,
                logoUrl: true,
              },
            },
          },
        },
      },
    });

    if (!user) {
      return null;
    }
    return user;
  }

  async updateAvatar({ id, avatarUrl }: { id: number; avatarUrl: string }) {
    // Using updateMany here since if the user already has a profile it would throw an error
    // because no records were found to update the profile picture
    await this.prismaClient.user.updateMany({
      where: {
        id,
        avatarUrl: {
          equals: null,
        },
      },
      data: {
        avatarUrl,
      },
    });
  }
  async findUserWithCredentials({ id }: { id: number }) {
    const user = await this.prismaClient.user.findUnique({
      where: {
        id,
      },
      select: {
        credentials: {
          select: credentialForCalendarServiceSelect,
        },
        timeZone: true,
        id: true,
        selectedCalendars: true,
      },
    });

    if (!user) {
      return null;
    }

    const { credentials, ...userWithSelectedCalendars } = withSelectedCalendars(user);
    return {
      ...userWithSelectedCalendars,
      credentials: buildNonDelegationCredentials(credentials),
    };
  }

  async findUnlockedUserForSession({ userId }: { userId: number }) {
    const user = await this.prismaClient.user.findUnique({
      where: {
        id: userId,
        // Locked users can't login
        locked: false,
      },
      select: {
        id: true,
        uuid: true,
        username: true,
        name: true,
        email: true,
        emailVerified: true,
        bio: true,
        avatarUrl: true,
        timeZone: true,
        weekStart: true,
        defaultScheduleId: true,
        bufferTime: true,
        theme: true,
        appTheme: true,
        createdDate: true,
        hideBranding: true,
        twoFactorEnabled: true,
        identityProvider: true,
        identityProviderId: true,
        brandColor: true,
        darkBrandColor: true,
        movedToProfileId: true,
        selectedCalendars: {
          select: {
            id: true,
            eventTypeId: true,
            externalId: true,
            integration: true,
            updatedAt: true,
            googleChannelId: true,
          },
        },
        completedOnboarding: true,
        destinationCalendar: true,
        locale: true,
        timeFormat: true,
        trialEndsAt: true,
        metadata: true,
        role: true,
        allowDynamicBooking: true,
        allowSEOIndexing: true,
        receiveMonthlyDigestEmail: true,
        requiresBookerEmailVerification: true,
        profiles: true,
      },
    });

    if (!user) {
      return null;
    }

    return withSelectedCalendars(user);
  }

  async getUserStats({ userId }: { userId: number }) {
    const user = await this.prismaClient.user.findUnique({
      where: {
        id: userId,
      },
      select: {
        _count: {
          select: {
            bookings: true,
            // We only need user level selected calendars
            selectedCalendars: {
              where: {
                eventTypeId: null,
              },
            },
            teams: true,
            eventTypes: true,
          },
        },
        teams: {
          select: {
            team: {
              select: {
                eventTypes: {
                  select: {
                    id: true,
                  },
                },
              },
            },
          },
        },
      },
    });

    if (!user) {
      return null;
    }

    const { _count, ...restUser } = user;
    const { selectedCalendars, ...restCount } = _count;
    return {
      ...restUser,
      _count: {
        ...restCount,
        userLevelSelectedCalendars: selectedCalendars,
      },
    };
  }

  async findManyByIdsIncludeDestinationAndSelectedCalendars({ ids }: { ids: number[] }) {
    const users = await this.prismaClient.user.findMany({
      where: { id: { in: ids } },
      include: {
        selectedCalendars: true,
        destinationCalendar: true,
      },
    });
    return users.map(withSelectedCalendars);
  }

  async updateStripeCustomerId({
    id,
    stripeCustomerId,
    existingMetadata,
  }: {
    id: number;
    stripeCustomerId: string;
    existingMetadata: z.infer<typeof userMetadata>;
  }) {
    return this.prismaClient.user.update({
      where: { id },
      data: { metadata: { ...existingMetadata, stripeCustomerId } },
    });
  }

  async findManyUsersForDynamicEventType({
    currentOrgDomain,
    usernameList,
  }: {
    currentOrgDomain: string | null;
    usernameList: string[];
  }) {
    const { where } = await this._getWhereClauseForFindingUsersByUsername({
      orgSlug: currentOrgDomain,
      usernameList,
    });

    // TODO: Should be moved to UserRepository
    return this.prismaClient.user.findMany({
      where,
      select: {
        locked: true,
        allowDynamicBooking: true,
        ...availabilityUserSelect,
        credentials: {
          select: credentialForCalendarServiceSelect,
        },
      },
    });
  }

  async findUsersByIds(userIds: number[]) {
    return this.prismaClient.user.findMany({
      where: {
        id: { in: userIds },
      },
      select: {
        id: true,
        name: true,
        email: true,
      },
    });
  }

  async findUsersWithLastBooking({ userIds, eventTypeId }: { userIds: number[]; eventTypeId: number }) {
    return this.prismaClient.user.findMany({
      where: {
        id: {
          in: userIds,
        },
      },
      select: {
        id: true,
        bookings: {
          select: {
            createdAt: true,
          },
          where: {
            eventTypeId,
            status: BookingStatus.ACCEPTED,
            attendees: {
              some: {
                noShow: false,
              },
            },
            OR: [
              {
                noShowHost: false,
              },
              {
                noShowHost: null,
              },
            ],
          },
          orderBy: {
            createdAt: "desc",
          },
          take: 1,
        },
      },
    });
  }

  async findUserWithHideBranding({ userId }: { userId: number }) {
    return this.prismaClient.user.findUnique({
      where: { id: userId },
      select: {
        hideBranding: true,
        profiles: {
          select: {
            organization: {
              select: {
                hideBranding: true,
              },
            },
          },
        },
      },
    });
  }

  async findByIdWithCredentialsAndCalendar({ userId }: { userId: number }) {
    return this.prismaClient.user.findUnique({
      where: { id: userId },
      select: {
        id: true,
        username: true,
        email: true,
        name: true,
        timeZone: true,
        locale: true,
        timeFormat: true,
        metadata: true,
        credentials: {
          select: credentialForCalendarServiceSelect,
        },
        destinationCalendar: true,
      },
    });
  }

  async findForPasswordReset({ id }: { id: number }) {
    return this.prismaClient.user.findUnique({
      where: { id },
      select: {
        email: true,
        name: true,
        locale: true,
      },
    });
  }

  async deleteMany({ userIds }: {userIds: number[]}){
    await this.prismaClient.user.deleteMany({
      where: {
        id: { in: userIds },
      },
    });
  }

  /**
   * Finds a user by ID returning only their username
   * @param userId - The user ID
   * @returns User with username or null
   */
  async findByIdWithUsername(userId: number): Promise<{ username: string | null } | null> {
    return this.prismaClient.user.findUnique({
      where: { id: userId },
      select: { username: true },
    });
  }

  async findManyByIdsWithCredentialsAndSelectedCalendars({ userIds }: { userIds: number[] }) {
    const users = await this.prismaClient.user.findMany({
      where: {
        id: {
          in: userIds,
        },
      },
      select: {
        ...prismaUserSelect, // Use the proper userSelect from @calcom/prisma/selects/user which includes schedules
        credentials: {
          select: credentialForCalendarServiceSelect,
        },
        selectedCalendars: {
          select: {
            eventTypeId: true,
          },
        },
      },
    });
    return users.map(withSelectedCalendars);
  }

  async findByEmailAndTeamId({ email, teamId }: { email: string; teamId: number }) {
    return this.prismaClient.user.findFirst({
      where: {
        email: email.toLowerCase(),
        teams: {
          some: {
            teamId,
            accepted: true,
          },
        },
      },
      select: userSelect,
    });
  }

  async findByIdWithSelectedCalendars({ userId }: { userId: number }) {
    return this.prismaClient.user.findUnique({
      where: { id: userId },
      select: {
        id: true,
        email: true,
        selectedCalendars: true,
        destinationCalendar: true,
      },
    });
  }

  async findByEmailWithInvitedTo({ email }: { email: string } ) {
    return this.prismaClient.user.findUnique({
      where: {
        email: email.toLowerCase()
      },
      select: {
        invitedTo: true
      }
    })
  }

  async findByUsernameAndOrganizationId({
    username,
    organizationId,
    excludeEmail,
  }: {
    username: string,
    organizationId: number | null,
    excludeEmail: string
  }) {
    return this.prismaClient.user.findFirst({
      where: {
        username,
        organizationId,
        NOT: { email: excludeEmail }
      },
      select: {
        id: true
      }
    })
  }

  async lockByEmail({ email }: { email: string }) {
    await this.prismaClient.user.updateMany({
      where: { email },
      data: { locked: true },
    });
  }

  async unlockByEmail({
    email,
  }: {
    email: string;
  }): Promise<{ email: string; username: string | null } | null> {
    const user = await this.prismaClient.user.findFirst({
      where: { email, locked: true },
      select: { id: true, email: true, username: true },
    });

    if (!user) return null;

    await this.prismaClient.user.update({
      where: { id: user.id },
      data: { locked: false },
    });

    return { email: user.email, username: user.username };
  }

  async upsertForSignup({
    email,
    username,
    hashedPassword,
    organizationId,
    emailVerified,
    identityProvider,
  }: {
    email: string;
    username: string | null;
    hashedPassword: string;
    organizationId: number | null;
    emailVerified: Date;
    identityProvider: IdentityProvider;
  }) {
    return this.prismaClient.user.upsert({
      where: { email },
      update: {
        username,
        emailVerified,
        identityProvider,
        password: {
          upsert: {
            create: { hash: hashedPassword },
            update: { hash: hashedPassword },
          },
        },
        organizationId,
      },
      create: {
        username,
        email,
        emailVerified,
        identityProvider,
        password: { create: { hash: hashedPassword } },
        organizationId,
      },
      select: { id: true },
    });
  }
  async listUsers({
    searchTerm,
    cursor,
    limit,
  }: {
    searchTerm?: string | null;
    cursor: number | null | undefined;
    limit?: number | null;
  }) {
    const bothLockedAndUnlockedWhere: Prisma.UserWhereInput = {
      OR: [{ locked: false }, { locked: true }],
    };
    const trimmedSearchTerm = searchTerm?.trim();
    const searchFilters: Prisma.UserWhereInput = trimmedSearchTerm
      ? {
        AND: [
          // To bypass the excludeLockedUsersExtension
          bothLockedAndUnlockedWhere,
          {
            OR: [
              { email: { contains: trimmedSearchTerm, mode: "insensitive" } },
              { username: { contains: trimmedSearchTerm, mode: "insensitive" } },
              {
                profiles: {
                  some: {
                    username: { contains: trimmedSearchTerm, mode: "insensitive" },
                  },
                },
              },
            ],
          },
        ],
      }
      // To bypass the excludeLockedUsersExtension
      : bothLockedAndUnlockedWhere;

    const hasLimit = limit !== undefined && limit !== null;
    const take = hasLimit ? limit + 1 : undefined; // +1 lets us detect "has more" for the cursor

    const users = await this.prismaClient.user.findMany({
      cursor: cursor ? { id: cursor } : undefined,
      skip: cursor ? 1 : 0,
      ...(take !== undefined ? { take } : {}),
      where: searchFilters,
      orderBy: {
        id: "asc",
      },
      select: {
        id: true,
        locked: true,
        email: true,
        username: true,
        name: true,
        timeZone: true,
        role: true,
        profiles: {
          select: {
            username: true,
          },
        },
      },
    });

    if (!hasLimit) {
      return { users, nextCursor: undefined, total: users.length };
    }

    const total = await this.prismaClient.user.count({
      where: searchFilters,
    });
    const hasMore = users.length > limit;
    const items = hasMore ? users.slice(0, limit) : users;
    const nextCursor = hasMore ? items[items.length - 1].id : undefined;
    return { users: items, nextCursor, total };
  }
}