import { MembershipRole, UserPlan } from "@prisma/client";
import { Prisma } from "@prisma/client";
import { randomBytes } from "crypto";
import { z } from "zod";

import {
  addSeat,
  removeSeat,
  getTeamSeatStats,
  downgradeTeamMembers,
  upgradeTeam,
  ensureSubscriptionQuantityCorrectness,
} from "@calcom/stripe/team-billing";

import { BASE_URL } from "@lib/config/constants";
import { HOSTED_CAL_FEATURES } from "@lib/config/constants";
import { sendTeamInviteEmail } from "@lib/emails/email-manager";
import { TeamInvite } from "@lib/emails/templates/team-invite-email";
import { getUserAvailability } from "@lib/queries/availability";
import { getTeamWithMembers, isTeamAdmin, isTeamOwner } from "@lib/queries/teams";
import slugify from "@lib/slugify";

import { createProtectedRouter } from "@server/createRouter";
import { getTranslation } from "@server/lib/i18n";
import { TRPCError } from "@trpc/server";

export const viewerTeamsRouter = createProtectedRouter()
  // Retrieves team by id
  .query("get", {
    input: z.object({
      teamId: z.number(),
    }),
    async resolve({ ctx, input }) {
      const team = await getTeamWithMembers(input.teamId);
      if (!team?.members.find((m) => m.id === ctx.user.id)) {
        throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not a member of this team." });
      }
      const membership = team?.members.find((membership) => membership.id === ctx.user.id);
      return {
        ...team,
        membership: {
          role: membership?.role as MembershipRole,
          isMissingSeat: membership?.plan === UserPlan.FREE,
        },
        requiresUpgrade: HOSTED_CAL_FEATURES ? !!team.members.find((m) => m.plan !== UserPlan.PRO) : false,
      };
    },
  })
  // Returns teams I a member of
  .query("list", {
    async resolve({ ctx }) {
      const memberships = await ctx.prisma.membership.findMany({
        where: {
          userId: ctx.user.id,
        },
        orderBy: { role: "desc" },
      });
      const teams = await ctx.prisma.team.findMany({
        where: {
          id: {
            in: memberships.map((membership) => membership.teamId),
          },
        },
      });

      return memberships.map((membership) => ({
        role: membership.role,
        accepted: membership.accepted,
        ...teams.find((team) => team.id === membership.teamId),
      }));
    },
  })
  .mutation("create", {
    input: z.object({
      name: z.string(),
    }),
    async resolve({ ctx, input }) {
      if (ctx.user.plan === "FREE") {
        throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not a pro user." });
      }

      const slug = slugify(input.name);

      const nameCollisions = await ctx.prisma.team.count({
        where: {
          OR: [{ name: input.name }, { slug: slug }],
        },
      });

      if (nameCollisions > 0)
        throw new TRPCError({ code: "BAD_REQUEST", message: "Team name already taken." });

      const createTeam = await ctx.prisma.team.create({
        data: {
          name: input.name,
          slug: slug,
        },
      });

      await ctx.prisma.membership.create({
        data: {
          teamId: createTeam.id,
          userId: ctx.user.id,
          role: "OWNER",
          accepted: true,
        },
      });
    },
  })
  // Allows team owner to update team metadata
  .mutation("update", {
    input: z.object({
      id: z.number(),
      bio: z.string().optional(),
      name: z.string().optional(),
      logo: z.string().optional(),
      slug: z.string().optional(),
      hideBranding: z.boolean().optional(),
    }),
    async resolve({ ctx, input }) {
      if (!(await isTeamAdmin(ctx.user?.id, input.id))) throw new TRPCError({ code: "UNAUTHORIZED" });

      if (input.slug) {
        const userConflict = await ctx.prisma.team.findMany({
          where: {
            slug: input.slug,
          },
        });
        if (userConflict.some((t) => t.id !== input.id)) return;
      }
      await ctx.prisma.team.update({
        where: {
          id: input.id,
        },
        data: {
          name: input.name,
          slug: input.slug,
          logo: input.logo,
          bio: input.bio,
          hideBranding: input.hideBranding,
        },
      });
    },
  })
  .mutation("delete", {
    input: z.object({
      teamId: z.number(),
    }),
    async resolve({ ctx, input }) {
      if (!(await isTeamOwner(ctx.user?.id, input.teamId))) throw new TRPCError({ code: "UNAUTHORIZED" });

      if (process.env.STRIPE_PRIVATE_KEY) {
        await downgradeTeamMembers(input.teamId);
      }

      // delete all memberships
      await ctx.prisma.membership.deleteMany({
        where: {
          teamId: input.teamId,
        },
      });

      await ctx.prisma.team.delete({
        where: {
          id: input.teamId,
        },
      });
    },
  })
  // Allows owner to remove member from team
  .mutation("removeMember", {
    input: z.object({
      teamId: z.number(),
      memberId: z.number(),
    }),
    async resolve({ ctx, input }) {
      if (!(await isTeamAdmin(ctx.user?.id, input.teamId))) throw new TRPCError({ code: "UNAUTHORIZED" });

      if (ctx.user?.id === input.memberId)
        throw new TRPCError({
          code: "FORBIDDEN",
          message: "You can not remove yourself from a team you own.",
        });

      await ctx.prisma.membership.delete({
        where: {
          userId_teamId: { userId: input.memberId, teamId: input.teamId },
        },
      });

      if (HOSTED_CAL_FEATURES) await removeSeat(ctx.user.id, input.teamId, input.memberId);
    },
  })
  .mutation("inviteMember", {
    input: z.object({
      teamId: z.number(),
      usernameOrEmail: z.string(),
      role: z.nativeEnum(MembershipRole),
      language: z.string(),
      sendEmailInvitation: z.boolean(),
    }),
    async resolve({ ctx, input }) {
      if (!(await isTeamAdmin(ctx.user?.id, input.teamId))) throw new TRPCError({ code: "UNAUTHORIZED" });

      const translation = await getTranslation(input.language ?? "en", "common");

      const team = await ctx.prisma.team.findFirst({
        where: {
          id: input.teamId,
        },
      });

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

      const invitee = await ctx.prisma.user.findFirst({
        where: {
          OR: [{ username: input.usernameOrEmail }, { email: input.usernameOrEmail }],
        },
      });

      let inviteeUserId: number | undefined = invitee?.id;

      if (!invitee) {
        // liberal email match
        const isEmail = (str: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);

        if (!isEmail(input.usernameOrEmail))
          throw new TRPCError({
            code: "NOT_FOUND",
            message: `Invite failed because there is no corresponding user for ${input.usernameOrEmail}`,
          });

        // valid email given, create User and add to team
        const user = await ctx.prisma.user.create({
          data: {
            email: input.usernameOrEmail,
            invitedTo: input.teamId,
            teams: {
              create: {
                teamId: input.teamId,
                role: input.role as MembershipRole,
              },
            },
          },
        });
        inviteeUserId = user.id;

        const token: string = randomBytes(32).toString("hex");

        await ctx.prisma.verificationToken.create({
          data: {
            identifier: input.usernameOrEmail,
            token,
            expires: new Date(new Date().setHours(168)), // +1 week
          },
        });

        if (ctx?.user?.name && team?.name) {
          const teamInviteEvent: TeamInvite = {
            language: translation,
            from: ctx.user.name,
            to: input.usernameOrEmail,
            teamName: team.name,
            joinLink: `${BASE_URL}/auth/signup?token=${token}&callbackUrl=${BASE_URL + "/settings/teams"}`,
          };
          await sendTeamInviteEmail(teamInviteEvent);
        }
      } else {
        // create provisional membership
        try {
          await ctx.prisma.membership.create({
            data: {
              teamId: input.teamId,
              userId: invitee.id,
              role: input.role as MembershipRole,
            },
          });
        } catch (e) {
          if (e instanceof Prisma.PrismaClientKnownRequestError) {
            if (e.code === "P2002") {
              throw new TRPCError({
                code: "FORBIDDEN",
                message: "This user is a member of this team / has a pending invitation.",
              });
            }
          } else throw e;
        }

        // inform user of membership by email
        if (input.sendEmailInvitation && ctx?.user?.name && team?.name) {
          const teamInviteEvent: TeamInvite = {
            language: translation,
            from: ctx.user.name,
            to: input.usernameOrEmail,
            teamName: team.name,
            joinLink: BASE_URL + "/settings/teams",
          };

          await sendTeamInviteEmail(teamInviteEvent);
        }
      }
      try {
        if (HOSTED_CAL_FEATURES) await addSeat(ctx.user.id, team.id, inviteeUserId);
      } catch (e) {
        console.log(e);
      }
    },
  })
  .mutation("acceptOrLeave", {
    input: z.object({
      teamId: z.number(),
      accept: z.boolean(),
    }),
    async resolve({ ctx, input }) {
      if (input.accept) {
        await ctx.prisma.membership.update({
          where: {
            userId_teamId: { userId: ctx.user.id, teamId: input.teamId },
          },
          data: {
            accepted: true,
          },
        });
      } else {
        try {
          //get team owner so we can alter their subscription seat count
          const teamOwner = await ctx.prisma.membership.findFirst({
            where: { teamId: input.teamId, role: MembershipRole.OWNER },
          });

          // TODO: disable if not hosted by Cal
          if (teamOwner) await removeSeat(teamOwner.userId, input.teamId, ctx.user.id);
        } catch (e) {
          console.log(e);
        }
        await ctx.prisma.membership.delete({
          where: {
            userId_teamId: { userId: ctx.user.id, teamId: input.teamId },
          },
        });
      }
    },
  })
  .mutation("changeMemberRole", {
    input: z.object({
      teamId: z.number(),
      memberId: z.number(),
      role: z.nativeEnum(MembershipRole),
    }),
    async resolve({ ctx, input }) {
      if (!(await isTeamAdmin(ctx.user?.id, input.teamId))) throw new TRPCError({ code: "UNAUTHORIZED" });

      const memberships = await ctx.prisma.membership.findMany({
        where: {
          teamId: input.teamId,
        },
      });

      const targetMembership = memberships.find((m) => m.userId === input.memberId);
      const myMembership = memberships.find((m) => m.userId === ctx.user.id);
      const teamHasMoreThanOneOwner = memberships.some((m) => m.role === MembershipRole.OWNER);

      if (myMembership?.role === MembershipRole.ADMIN && targetMembership?.role === MembershipRole.OWNER) {
        throw new TRPCError({
          code: "FORBIDDEN",
          message: "You can not change the role of an owner if you are an admin.",
        });
      }

      if (!teamHasMoreThanOneOwner) {
        throw new TRPCError({
          code: "FORBIDDEN",
          message: "You can not change the role of the only owner of a team.",
        });
      }

      if (myMembership?.role === MembershipRole.ADMIN && input.memberId === ctx.user.id) {
        throw new TRPCError({
          code: "FORBIDDEN",
          message: "You can not change yourself to a higher role.",
        });
      }

      await ctx.prisma.membership.update({
        where: {
          userId_teamId: { userId: input.memberId, teamId: input.teamId },
        },
        data: {
          role: input.role,
        },
      });
    },
  })
  .query("getMemberAvailability", {
    input: z.object({
      teamId: z.number(),
      memberId: z.number(),
      timezone: z.string(),
      dateFrom: z.string(),
      dateTo: z.string(),
    }),
    async resolve({ ctx, input }) {
      const team = await isTeamAdmin(ctx.user?.id, input.teamId);
      if (!team) throw new TRPCError({ code: "UNAUTHORIZED" });

      // verify member is in team
      const members = await ctx.prisma.membership.findMany({
        where: { teamId: input.teamId },
        include: { user: true },
      });
      const member = members?.find((m) => m.userId === input.memberId);
      if (!member) throw new TRPCError({ code: "NOT_FOUND", message: "Member not found" });
      if (!member.user.username)
        throw new TRPCError({ code: "BAD_REQUEST", message: "Member doesn't have a username" });

      // get availability for this member
      return await getUserAvailability({
        username: member.user.username,
        timezone: input.timezone,
        dateFrom: input.dateFrom,
        dateTo: input.dateTo,
      });
    },
  })
  .mutation("upgradeTeam", {
    input: z.object({
      teamId: z.number(),
    }),
    async resolve({ ctx, input }) {
      if (!HOSTED_CAL_FEATURES)
        throw new TRPCError({ code: "FORBIDDEN", message: "Team billing is not enabled" });
      return await upgradeTeam(ctx.user.id, input.teamId);
    },
  })
  .query("getTeamSeats", {
    input: z.object({
      teamId: z.number(),
    }),
    async resolve({ input }) {
      return await getTeamSeatStats(input.teamId);
    },
  })
  .mutation("ensureSubscriptionQuantityCorrectness", {
    input: z.object({
      teamId: z.number(),
    }),
    async resolve({ ctx, input }) {
      return await ensureSubscriptionQuantityCorrectness(ctx.user.id, input.teamId);
    },
  });