import type { DestinationCalendar, Prisma } from "@prisma/client";
import { AppCategories, BookingStatus, IdentityProvider } from "@prisma/client";
import { reverse } from "lodash";
import type { NextApiResponse } from "next";
import { authenticator } from "otplib";
import z from "zod";

import ethRouter from "@calcom/app-store/rainbow/trpc/router";
import app_RoutingForms from "@calcom/app-store/routing-forms/trpc-router";
import { deleteStripeCustomer } from "@calcom/app-store/stripepayment/lib/customer";
import stripe from "@calcom/app-store/stripepayment/lib/server";
import { getPremiumPlanProductId } from "@calcom/app-store/stripepayment/lib/utils";
import getApps, { getLocationGroupedOptions } from "@calcom/app-store/utils";
import { cancelScheduledJobs } from "@calcom/app-store/zapier/lib/nodeScheduler";
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
import { DailyLocationType } from "@calcom/core/location";
import {
  getDownloadLinkOfCalVideoByRecordingId,
  getRecordingsOfCalVideoByRoomName,
} from "@calcom/core/videoClient";
import dayjs from "@calcom/dayjs";
import { sendCancelledEmails, sendFeedbackEmail } from "@calcom/emails";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import { samlTenantProduct } from "@calcom/features/ee/sso/lib/saml";
import { userAdminRouter } from "@calcom/features/ee/users/server/trpc-router";
import { featureFlagRouter } from "@calcom/features/flags/server/router";
import { insightsRouter } from "@calcom/features/insights/server/trpc-router";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import getEnabledApps from "@calcom/lib/apps/getEnabledApps";
import { FULL_NAME_LENGTH_MAX_LIMIT, IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
import { deletePayment } from "@calcom/lib/payment/deletePayment";
import { checkUsername } from "@calcom/lib/server/checkUsername";
import { getTranslation } from "@calcom/lib/server/i18n";
import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image";
import slugify from "@calcom/lib/slugify";
import {
  deleteWebUser as syncServicesDeleteWebUser,
  updateWebUser as syncServicesUpdateWebUser,
} from "@calcom/lib/sync/SyncServiceManager";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import { EventTypeMetaDataSchema, userMetadata } from "@calcom/prisma/zod-utils";

import { TRPCError } from "@trpc/server";

import { authedProcedure, getLocale, mergeRouters, publicProcedure, router } from "../trpc";
import { apiKeysRouter } from "./viewer/apiKeys";
import { appsRouter } from "./viewer/apps";
import { authRouter } from "./viewer/auth";
import { availabilityRouter } from "./viewer/availability";
import { bookingsRouter } from "./viewer/bookings";
import { deploymentSetupRouter } from "./viewer/deploymentSetup";
import { eventTypesRouter } from "./viewer/eventTypes";
import { paymentsRouter } from "./viewer/payments";
import { slotsRouter } from "./viewer/slots";
import { ssoRouter } from "./viewer/sso";
import { viewerTeamsRouter } from "./viewer/teams";
import { webhookRouter } from "./viewer/webhook";
import { workflowsRouter } from "./viewer/workflows";

// things that unauthenticated users can query about themselves
const publicViewerRouter = router({
  session: publicProcedure.query(({ ctx }) => {
    return ctx.session;
  }),
  i18n: publicProcedure.query(async ({ ctx }) => {
    const { locale, i18n } = await getLocale(ctx);

    return {
      i18n,
      locale,
    };
  }),
  countryCode: publicProcedure.query(({ ctx }) => {
    const { req } = ctx;

    const countryCode: string | string[] = req?.headers?.["x-vercel-ip-country"] ?? "";
    return { countryCode: Array.isArray(countryCode) ? countryCode[0] : countryCode };
  }),
  samlTenantProduct: publicProcedure
    .input(
      z.object({
        email: z.string().email(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const { prisma } = ctx;
      const { email } = input;

      return await samlTenantProduct(prisma, email);
    }),
  stripeCheckoutSession: publicProcedure
    .input(
      z.object({
        stripeCustomerId: z.string().optional(),
        checkoutSessionId: z.string().optional(),
      })
    )
    .query(async ({ input }) => {
      const { checkoutSessionId, stripeCustomerId } = input;

      // TODO: Move the following data checks to superRefine
      if (!checkoutSessionId && !stripeCustomerId) {
        throw new Error("Missing checkoutSessionId or stripeCustomerId");
      }

      if (checkoutSessionId && stripeCustomerId) {
        throw new Error("Both checkoutSessionId and stripeCustomerId provided");
      }
      let customerId: string;
      let isPremiumUsername = false;
      let hasPaymentFailed = false;
      if (checkoutSessionId) {
        try {
          const session = await stripe.checkout.sessions.retrieve(checkoutSessionId);
          if (typeof session.customer !== "string") {
            return {
              valid: false,
            };
          }
          customerId = session.customer;
          isPremiumUsername = true;
          hasPaymentFailed = session.payment_status !== "paid";
        } catch (e) {
          return {
            valid: false,
          };
        }
      } else {
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        customerId = stripeCustomerId!;
      }

      try {
        const customer = await stripe.customers.retrieve(customerId);
        if (customer.deleted) {
          return {
            valid: false,
          };
        }

        return {
          valid: true,
          hasPaymentFailed,
          isPremiumUsername,
          customer: {
            username: customer.metadata.username,
            email: customer.metadata.email,
            stripeCustomerId: customerId,
          },
        };
      } catch (e) {
        return {
          valid: false,
        };
      }
    }),
  slots: slotsRouter,
  cityTimezones: publicProcedure.query(async () => {
    /**
     * Lazy loads third party dependency to avoid loading 1.5Mb for ALL tRPC procedures.
     * Thanks @roae for the tip 🙏
     **/
    const allCities = await import("city-timezones").then((mod) => mod.cityMapping);
    /**
     * Filter out all cities that have the same "city" key and only use the one with the highest population.
     * This way we return a new array of cities without running the risk of having more than one city
     * with the same name on the dropdown and prevent users from mistaking the time zone of the desired city.
     */
    const topPopulatedCities: { [key: string]: { city: string; timezone: string; pop: number } } = {};
    allCities.forEach((city) => {
      const cityPopulationCount = city.pop;
      if (
        topPopulatedCities[city.city]?.pop === undefined ||
        cityPopulationCount > topPopulatedCities[city.city].pop
      ) {
        topPopulatedCities[city.city] = { city: city.city, timezone: city.timezone, pop: city.pop };
      }
    });
    const uniqueCities = Object.values(topPopulatedCities);
    /** Add specific overries in here */
    uniqueCities.forEach((city) => {
      if (city.city === "London") city.timezone = "Europe/London";
      if (city.city === "Londonderry") city.city = "London";
    });
    return uniqueCities;
  }),
});

// routes only available to authenticated users
const loggedInViewerRouter = router({
  me: authedProcedure.query(async ({ ctx }) => {
    const { user } = ctx;
    // Destructuring here only makes it more illegible
    // pick only the part we want to expose in the API
    return {
      id: user.id,
      name: user.name,
      username: user.username,
      email: user.email,
      startTime: user.startTime,
      endTime: user.endTime,
      bufferTime: user.bufferTime,
      locale: user.locale,
      timeFormat: user.timeFormat,
      timeZone: user.timeZone,
      avatar: user.avatar,
      createdDate: user.createdDate,
      trialEndsAt: user.trialEndsAt,
      defaultScheduleId: user.defaultScheduleId,
      completedOnboarding: user.completedOnboarding,
      twoFactorEnabled: user.twoFactorEnabled,
      disableImpersonation: user.disableImpersonation,
      identityProvider: user.identityProvider,
      brandColor: user.brandColor,
      darkBrandColor: user.darkBrandColor,
      away: user.away,
      bio: user.bio,
      weekStart: user.weekStart,
      theme: user.theme,
      hideBranding: user.hideBranding,
      metadata: user.metadata,
    };
  }),
  avatar: authedProcedure.query(({ ctx }) => ({
    avatar: ctx.user.rawAvatar,
  })),
  deleteMe: authedProcedure
    .input(
      z.object({
        password: z.string(),
        totpCode: z.string().optional(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      // Check if input.password is correct
      const user = await prisma.user.findUnique({
        where: {
          email: ctx.user.email.toLowerCase(),
        },
      });
      if (!user) {
        throw new Error(ErrorCode.UserNotFound);
      }

      if (user.identityProvider !== IdentityProvider.CAL) {
        throw new Error(ErrorCode.ThirdPartyIdentityProviderEnabled);
      }

      if (!user.password) {
        throw new Error(ErrorCode.UserMissingPassword);
      }

      const isCorrectPassword = await verifyPassword(input.password, user.password);
      if (!isCorrectPassword) {
        throw new Error(ErrorCode.IncorrectPassword);
      }

      if (user.twoFactorEnabled) {
        if (!input.totpCode) {
          throw new Error(ErrorCode.SecondFactorRequired);
        }

        if (!user.twoFactorSecret) {
          console.error(`Two factor is enabled for user ${user.id} but they have no secret`);
          throw new Error(ErrorCode.InternalServerError);
        }

        if (!process.env.CALENDSO_ENCRYPTION_KEY) {
          console.error(`"Missing encryption key; cannot proceed with two factor login."`);
          throw new Error(ErrorCode.InternalServerError);
        }

        const secret = symmetricDecrypt(user.twoFactorSecret, process.env.CALENDSO_ENCRYPTION_KEY);
        if (secret.length !== 32) {
          console.error(
            `Two factor secret decryption failed. Expected key with length 32 but got ${secret.length}`
          );
          throw new Error(ErrorCode.InternalServerError);
        }

        // If user has 2fa enabled, check if input.totpCode is correct
        const isValidToken = authenticator.check(input.totpCode, secret);
        if (!isValidToken) {
          throw new Error(ErrorCode.IncorrectTwoFactorCode);
        }
      }

      // If 2FA is disabled or totpCode is valid then delete the user from stripe and database
      await deleteStripeCustomer(user).catch(console.warn);
      // Remove my account
      const deletedUser = await ctx.prisma.user.delete({
        where: {
          id: ctx.user.id,
        },
      });

      // Sync Services
      syncServicesDeleteWebUser(deletedUser);
      return;
    }),
  deleteMeWithoutPassword: authedProcedure.mutation(async ({ ctx }) => {
    const user = await prisma.user.findUnique({
      where: {
        email: ctx.user.email.toLowerCase(),
      },
    });
    if (!user) {
      throw new Error(ErrorCode.UserNotFound);
    }

    if (user.identityProvider === IdentityProvider.CAL) {
      throw new Error(ErrorCode.SocialIdentityProviderRequired);
    }

    if (user.twoFactorEnabled) {
      throw new Error(ErrorCode.SocialIdentityProviderRequired);
    }

    // Remove me from Stripe
    await deleteStripeCustomer(user).catch(console.warn);

    // Remove my account
    const deletedUser = await ctx.prisma.user.delete({
      where: {
        id: ctx.user.id,
      },
    });
    // Sync Services
    syncServicesDeleteWebUser(deletedUser);

    return;
  }),
  away: authedProcedure
    .input(
      z.object({
        away: z.boolean(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      await ctx.prisma.user.update({
        where: {
          email: ctx.user.email,
        },
        data: {
          away: input.away,
        },
      });
    }),
  connectedCalendars: authedProcedure.query(async ({ ctx }) => {
    const { user, prisma } = ctx;

    const userCredentials = await prisma.credential.findMany({
      where: {
        userId: ctx.user.id,
        app: {
          categories: { has: AppCategories.calendar },
          enabled: true,
        },
      },
    });

    // get user's credentials + their connected integrations
    const calendarCredentials = getCalendarCredentials(userCredentials);

    // get all the connected integrations' calendars (from third party)
    const { connectedCalendars, destinationCalendar } = await getConnectedCalendars(
      calendarCredentials,
      user.selectedCalendars,
      user.destinationCalendar?.externalId
    );

    if (connectedCalendars.length === 0) {
      /* As there are no connected calendars, delete the destination calendar if it exists */
      if (user.destinationCalendar) {
        await ctx.prisma.destinationCalendar.delete({
          where: { userId: user.id },
        });
        user.destinationCalendar = null;
      }
    } else if (!user.destinationCalendar) {
      /*
        There are connected calendars, but no destination calendar
        So create a default destination calendar with the first primary connected calendar
        */
      const { integration = "", externalId = "", credentialId, email } = connectedCalendars[0].primary ?? {};
      user.destinationCalendar = await ctx.prisma.destinationCalendar.create({
        data: {
          userId: user.id,
          integration,
          externalId,
          credentialId,
        },
      });
    } else {
      /* There are connected calendars and a destination calendar */

      // Check if destinationCalendar exists in connectedCalendars
      const allCals = connectedCalendars.map((cal) => cal.calendars ?? []).flat();
      const destinationCal = allCals.find(
        (cal) =>
          cal.externalId === user.destinationCalendar?.externalId &&
          cal.integration === user.destinationCalendar?.integration
      );

      if (!destinationCal) {
        // If destinationCalendar is out of date, update it with the first primary connected calendar
        const { integration = "", externalId = "" } = connectedCalendars[0].primary ?? {};
        user.destinationCalendar = await ctx.prisma.destinationCalendar.update({
          where: { userId: user.id },
          data: {
            integration,
            externalId,
          },
        });
      }
    }

    return {
      connectedCalendars,
      destinationCalendar: {
        ...(user.destinationCalendar as DestinationCalendar),
        ...destinationCalendar,
      },
    };
  }),
  setDestinationCalendar: authedProcedure
    .input(
      z.object({
        integration: z.string(),
        externalId: z.string(),
        eventTypeId: z.number().nullish(),
        bookingId: z.number().nullish(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const { user } = ctx;
      const { integration, externalId, eventTypeId } = input;
      const calendarCredentials = getCalendarCredentials(user.credentials);
      const { connectedCalendars } = await getConnectedCalendars(calendarCredentials, user.selectedCalendars);
      const allCals = connectedCalendars.map((cal) => cal.calendars ?? []).flat();

      const credentialId = allCals.find(
        (cal) => cal.externalId === externalId && cal.integration === integration && cal.readOnly === false
      )?.credentialId;

      if (!credentialId) {
        throw new TRPCError({ code: "BAD_REQUEST", message: `Could not find calendar ${input.externalId}` });
      }

      let where;

      if (eventTypeId) {
        if (
          !(await prisma.eventType.findFirst({
            where: {
              id: eventTypeId,
              userId: user.id,
            },
          }))
        ) {
          throw new TRPCError({
            code: "UNAUTHORIZED",
            message: `You don't have access to event type ${eventTypeId}`,
          });
        }

        where = { eventTypeId };
      } else where = { userId: user.id };

      await ctx.prisma.destinationCalendar.upsert({
        where,
        update: {
          integration,
          externalId,
          credentialId,
        },
        create: {
          ...where,
          integration,
          externalId,
          credentialId,
        },
      });
    }),
  integrations: authedProcedure
    .input(
      z.object({
        variant: z.string().optional(),
        exclude: z.array(z.string()).optional(),
        onlyInstalled: z.boolean().optional(),
      })
    )
    .query(async ({ ctx, input }) => {
      const { user } = ctx;
      const { variant, exclude, onlyInstalled } = input;
      const { credentials } = user;

      const enabledApps = await getEnabledApps(credentials);
      //TODO: Refactor this to pick up only needed fields and prevent more leaking
      let apps = enabledApps.map(
        ({ credentials: _, credential: _1, key: _2 /* don't leak to frontend */, ...app }) => {
          const credentialIds = credentials.filter((c) => c.type === app.type).map((c) => c.id);
          const invalidCredentialIds = credentials
            .filter((c) => c.type === app.type && c.invalid)
            .map((c) => c.id);
          return {
            ...app,
            credentialIds,
            invalidCredentialIds,
          };
        }
      );

      if (variant) {
        // `flatMap()` these work like `.filter()` but infers the types correctly
        apps = apps
          // variant check
          .flatMap((item) => (item.variant.startsWith(variant) ? [item] : []));
      }

      if (exclude) {
        // exclusion filter
        apps = apps.filter((item) => (exclude ? !exclude.includes(item.variant) : true));
      }

      if (onlyInstalled) {
        apps = apps.flatMap((item) => (item.credentialIds.length > 0 || item.isGlobal ? [item] : []));
      }
      return {
        items: apps,
      };
    }),
  appById: authedProcedure
    .input(
      z.object({
        appId: z.string(),
      })
    )
    .query(async ({ ctx, input }) => {
      const { user } = ctx;
      const appId = input.appId;
      const { credentials } = user;
      const apps = getApps(credentials);
      const appFromDb = apps.find((app) => app.slug === appId);
      if (!appFromDb) {
        throw new TRPCError({ code: "BAD_REQUEST", message: `Could not find app ${appId}` });
      }

      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      const { credential: _, credentials: _1, ...app } = appFromDb;
      return {
        isInstalled: appFromDb.credentials.length,
        ...app,
      };
    }),
  apps: authedProcedure
    .input(
      z.object({
        extendsFeature: z.literal("EventType"),
      })
    )
    .query(async ({ ctx, input }) => {
      const { user } = ctx;
      const { credentials } = user;

      const apps = await getEnabledApps(credentials);
      return apps
        .filter((app) => app.extendsFeature?.includes(input.extendsFeature))
        .map((app) => ({
          ...app,
          isInstalled: !!app.credentials?.length,
        }));
    }),
  appCredentialsByType: authedProcedure
    .input(
      z.object({
        appType: z.string(),
      })
    )
    .query(async ({ ctx, input }) => {
      const { user } = ctx;
      return user.credentials.filter((app) => app.type == input.appType).map((credential) => credential.id);
    }),
  stripeCustomer: authedProcedure.query(async ({ ctx }) => {
    const {
      user: { id: userId },
      prisma,
    } = ctx;

    const user = await prisma.user.findUnique({
      where: {
        id: userId,
      },
      select: {
        metadata: true,
      },
    });

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

    const metadata = userMetadata.parse(user.metadata);

    if (!metadata?.stripeCustomerId) {
      throw new TRPCError({ code: "BAD_REQUEST", message: "No stripe customer id" });
    }
    // Fetch stripe customer
    const stripeCustomerId = metadata?.stripeCustomerId;
    const customer = await stripe.customers.retrieve(stripeCustomerId);
    if (customer.deleted) {
      throw new TRPCError({ code: "BAD_REQUEST", message: "No stripe customer found" });
    }

    const username = customer?.metadata?.username || null;

    return {
      isPremium: !!metadata?.isPremium,
      username,
    };
  }),
  updateProfile: authedProcedure
    .input(
      z.object({
        username: z.string().optional(),
        name: z.string().max(FULL_NAME_LENGTH_MAX_LIMIT).optional(),
        email: z.string().optional(),
        bio: z.string().optional(),
        avatar: z.string().optional(),
        timeZone: z.string().optional(),
        weekStart: z.string().optional(),
        hideBranding: z.boolean().optional(),
        allowDynamicBooking: z.boolean().optional(),
        brandColor: z.string().optional(),
        darkBrandColor: z.string().optional(),
        theme: z.string().optional().nullable(),
        completedOnboarding: z.boolean().optional(),
        locale: z.string().optional(),
        timeFormat: z.number().optional(),
        disableImpersonation: z.boolean().optional(),
        metadata: userMetadata.optional(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const { user, prisma } = ctx;
      const data: Prisma.UserUpdateInput = {
        ...input,
        metadata: input.metadata as Prisma.InputJsonValue,
      };
      let isPremiumUsername = false;
      if (input.username) {
        const username = slugify(input.username);
        // Only validate if we're changing usernames
        if (username !== user.username) {
          data.username = username;
          const response = await checkUsername(username);
          isPremiumUsername = response.premium;
          if (!response.available) {
            throw new TRPCError({ code: "BAD_REQUEST", message: response.message });
          }
        }
      }
      if (input.avatar) {
        data.avatar = await resizeBase64Image(input.avatar);
      }
      const userToUpdate = await prisma.user.findUnique({
        where: {
          id: user.id,
        },
      });

      if (!userToUpdate) {
        throw new TRPCError({ code: "NOT_FOUND", message: "User not found" });
      }
      const metadata = userMetadata.parse(userToUpdate.metadata);

      const isPremium = metadata?.isPremium;
      if (isPremiumUsername) {
        const stripeCustomerId = metadata?.stripeCustomerId;
        if (!isPremium || !stripeCustomerId) {
          throw new TRPCError({ code: "BAD_REQUEST", message: "User is not premium" });
        }

        const stripeSubscriptions = await stripe.subscriptions.list({ customer: stripeCustomerId });

        if (!stripeSubscriptions || !stripeSubscriptions.data.length) {
          throw new TRPCError({
            code: "INTERNAL_SERVER_ERROR",
            message: "No stripeSubscription found",
          });
        }

        // Iterate over subscriptions and look for premium product id and status active
        // @TODO: iterate if stripeSubscriptions.hasMore is true
        const isPremiumUsernameSubscriptionActive = stripeSubscriptions.data.some(
          (subscription) =>
            subscription.items.data[0].price.product === getPremiumPlanProductId() &&
            subscription.status === "active"
        );

        if (!isPremiumUsernameSubscriptionActive) {
          throw new TRPCError({
            code: "BAD_REQUEST",
            message: "You need to pay for premium username",
          });
        }
      }

      const updatedUser = await prisma.user.update({
        where: {
          id: user.id,
        },
        data,
        select: {
          id: true,
          username: true,
          email: true,
          metadata: true,
          name: true,
          createdDate: true,
        },
      });

      // Sync Services
      await syncServicesUpdateWebUser(updatedUser);

      // Notify stripe about the change
      if (updatedUser && updatedUser.metadata && hasKeyInMetadata(updatedUser, "stripeCustomerId")) {
        const stripeCustomerId = `${updatedUser.metadata.stripeCustomerId}`;
        await stripe.customers.update(stripeCustomerId, {
          metadata: {
            username: updatedUser.username,
            email: updatedUser.email,
            userId: updatedUser.id,
          },
        });
      }
      // Revalidate booking pages
      const res = ctx.res as NextApiResponse;
      if (typeof res?.revalidate !== "undefined") {
        const eventTypes = await prisma.eventType.findMany({
          where: {
            userId: user.id,
            team: null,
            hidden: false,
          },
          select: {
            id: true,
            slug: true,
          },
        });
        // waiting for this isn't needed
        Promise.all(eventTypes.map((eventType) => res?.revalidate(`/${ctx.user.username}/${eventType.slug}`)))
          .then(() => console.info("Booking pages revalidated"))
          .catch((e) => console.error(e));
      }
    }),
  eventTypeOrder: authedProcedure
    .input(
      z.object({
        ids: z.array(z.number()),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const { prisma, user } = ctx;
      const allEventTypes = await ctx.prisma.eventType.findMany({
        select: {
          id: true,
        },
        where: {
          id: {
            in: input.ids,
          },
          OR: [
            {
              userId: user.id,
            },
            {
              users: {
                some: {
                  id: user.id,
                },
              },
            },
            {
              team: {
                members: {
                  some: {
                    userId: user.id,
                  },
                },
              },
            },
          ],
        },
      });
      const allEventTypeIds = new Set(allEventTypes.map((type) => type.id));
      if (input.ids.some((id) => !allEventTypeIds.has(id))) {
        throw new TRPCError({
          code: "UNAUTHORIZED",
        });
      }
      await Promise.all(
        reverse(input.ids).map((id, position) => {
          return prisma.eventType.update({
            where: {
              id,
            },
            data: {
              position,
            },
          });
        })
      );
    }),
  //Comment for PR: eventTypePosition is not used anywhere
  submitFeedback: authedProcedure
    .input(
      z.object({
        rating: z.string(),
        comment: z.string(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const { rating, comment } = input;

      const feedback = {
        username: ctx.user.username || "Nameless",
        email: ctx.user.email || "No email address",
        rating: rating,
        comment: comment,
      };

      await ctx.prisma.feedback.create({
        data: {
          date: dayjs().toISOString(),
          userId: ctx.user.id,
          rating: rating,
          comment: comment,
        },
      });

      if (process.env.SEND_FEEDBACK_EMAIL && comment) sendFeedbackEmail(feedback);
    }),
  locationOptions: authedProcedure.query(async ({ ctx }) => {
    const credentials = await prisma.credential.findMany({
      where: {
        userId: ctx.user.id,
      },
      select: {
        id: true,
        type: true,
        key: true,
        userId: true,
        appId: true,
        invalid: true,
      },
    });

    const integrations = await getEnabledApps(credentials);

    const t = await getTranslation(ctx.user.locale ?? "en", "common");

    const locationOptions = getLocationGroupedOptions(integrations, t);

    return locationOptions;
  }),
  deleteCredential: authedProcedure
    .input(
      z.object({
        id: z.number(),
        externalId: z.string().optional(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const { id, externalId } = input;

      const credential = await prisma.credential.findFirst({
        where: {
          id: id,
          userId: ctx.user.id,
        },
        select: {
          key: true,
          appId: true,
          app: {
            select: {
              slug: true,
              categories: true,
              dirName: true,
            },
          },
        },
      });

      if (!credential) {
        throw new TRPCError({ code: "NOT_FOUND" });
      }

      const eventTypes = await prisma.eventType.findMany({
        where: {
          userId: ctx.user.id,
        },
        select: {
          id: true,
          locations: true,
          destinationCalendar: {
            include: {
              credential: true,
            },
          },
          price: true,
          currency: true,
          metadata: true,
        },
      });

      // TODO: Improve this uninstallation cleanup per event by keeping a relation of EventType to App which has the data.
      for (const eventType of eventTypes) {
        if (eventType.locations) {
          // If it's a video, replace the location with Cal video
          if (credential.app?.categories.includes(AppCategories.video)) {
            // Find the user's event types

            // Look for integration name from app slug
            const integrationQuery =
              credential.app?.slug === "msteams" ? "office365_video" : credential.app?.slug.split("-")[0];

            // Check if the event type uses the deleted integration

            // To avoid type errors, need to stringify and parse JSON to use array methods
            const locationsSchema = z.array(z.object({ type: z.string() }));
            const locations = locationsSchema.parse(eventType.locations);

            const updatedLocations = locations.map((location: { type: string }) => {
              if (location.type.includes(integrationQuery)) {
                return { type: DailyLocationType };
              }
              return location;
            });

            await prisma.eventType.update({
              where: {
                id: eventType.id,
              },
              data: {
                locations: updatedLocations,
              },
            });
          }
        }

        // If it's a calendar, remove the destination calendar from the event type
        if (credential.app?.categories.includes(AppCategories.calendar)) {
          if (eventType.destinationCalendar?.credential?.appId === credential.appId) {
            const destinationCalendar = await prisma.destinationCalendar.findFirst({
              where: {
                id: eventType.destinationCalendar?.id,
              },
            });
            if (destinationCalendar) {
              await prisma.destinationCalendar.delete({
                where: {
                  id: destinationCalendar.id,
                },
              });
            }
          }

          if (externalId) {
            const existingSelectedCalendar = await prisma.selectedCalendar.findFirst({
              where: {
                externalId: externalId,
              },
            });
            // @TODO: SelectedCalendar doesn't have unique ID so we should only delete one item
            if (existingSelectedCalendar) {
              await prisma.selectedCalendar.delete({
                where: {
                  userId_integration_externalId: {
                    userId: existingSelectedCalendar.userId,
                    externalId: existingSelectedCalendar.externalId,
                    integration: existingSelectedCalendar.integration,
                  },
                },
              });
            }
          }
        }

        const metadata = EventTypeMetaDataSchema.parse(eventType.metadata);

        const stripeAppData = getPaymentAppData({ ...eventType, metadata });

        // If it's a payment, hide the event type and set the price to 0. Also cancel all pending bookings
        if (credential.app?.categories.includes(AppCategories.payment)) {
          if (stripeAppData.price) {
            await prisma.$transaction(async () => {
              await prisma.eventType.update({
                where: {
                  id: eventType.id,
                },
                data: {
                  hidden: true,
                  metadata: {
                    ...metadata,
                    apps: {
                      ...metadata?.apps,
                      stripe: {
                        ...metadata?.apps?.stripe,
                        price: 0,
                      },
                    },
                  },
                },
              });

              // Assuming that all bookings under this eventType need to be paid
              const unpaidBookings = await prisma.booking.findMany({
                where: {
                  userId: ctx.user.id,
                  eventTypeId: eventType.id,
                  status: "PENDING",
                  paid: false,
                  payment: {
                    every: {
                      success: false,
                    },
                  },
                },
                select: {
                  ...bookingMinimalSelect,
                  recurringEventId: true,
                  userId: true,
                  responses: true,
                  user: {
                    select: {
                      id: true,
                      credentials: true,
                      email: true,
                      timeZone: true,
                      name: true,
                      destinationCalendar: true,
                      locale: true,
                    },
                  },
                  location: true,
                  references: {
                    select: {
                      uid: true,
                      type: true,
                      externalCalendarId: true,
                    },
                  },
                  payment: true,
                  paid: true,
                  eventType: {
                    select: {
                      recurringEvent: true,
                      title: true,
                      bookingFields: true,
                      seatsPerTimeSlot: true,
                      seatsShowAttendees: true,
                    },
                  },
                  uid: true,
                  eventTypeId: true,
                  destinationCalendar: true,
                },
              });

              for (const booking of unpaidBookings) {
                await prisma.booking.update({
                  where: {
                    id: booking.id,
                  },
                  data: {
                    status: BookingStatus.CANCELLED,
                    cancellationReason: "Payment method removed",
                  },
                });

                for (const payment of booking.payment) {
                  try {
                    await deletePayment(payment.id, credential);
                  } catch (e) {
                    console.error(e);
                  }
                  await prisma.payment.delete({
                    where: {
                      id: payment.id,
                    },
                  });
                }

                await prisma.attendee.deleteMany({
                  where: {
                    bookingId: booking.id,
                  },
                });

                await prisma.bookingReference.deleteMany({
                  where: {
                    bookingId: booking.id,
                  },
                });

                const attendeesListPromises = booking.attendees.map(async (attendee) => {
                  return {
                    name: attendee.name,
                    email: attendee.email,
                    timeZone: attendee.timeZone,
                    language: {
                      translate: await getTranslation(attendee.locale ?? "en", "common"),
                      locale: attendee.locale ?? "en",
                    },
                  };
                });

                const attendeesList = await Promise.all(attendeesListPromises);
                const tOrganizer = await getTranslation(booking?.user?.locale ?? "en", "common");
                await sendCancelledEmails({
                  type: booking?.eventType?.title as string,
                  title: booking.title,
                  description: booking.description,
                  customInputs: isPrismaObjOrUndefined(booking.customInputs),
                  ...getCalEventResponses({
                    bookingFields: booking.eventType?.bookingFields ?? null,
                    booking,
                  }),
                  startTime: booking.startTime.toISOString(),
                  endTime: booking.endTime.toISOString(),
                  organizer: {
                    email: booking?.user?.email as string,
                    name: booking?.user?.name ?? "Nameless",
                    timeZone: booking?.user?.timeZone as string,
                    language: { translate: tOrganizer, locale: booking?.user?.locale ?? "en" },
                  },
                  attendees: attendeesList,
                  uid: booking.uid,
                  recurringEvent: parseRecurringEvent(booking.eventType?.recurringEvent),
                  location: booking.location,
                  destinationCalendar: booking.destinationCalendar || booking.user?.destinationCalendar,
                  cancellationReason: "Payment method removed by organizer",
                  seatsPerTimeSlot: booking.eventType?.seatsPerTimeSlot,
                  seatsShowAttendees: booking.eventType?.seatsShowAttendees,
                });
              }
            });
          }
        }
      }

      // if zapier get disconnected, delete zapier apiKey, delete zapier webhooks and cancel all scheduled jobs from zapier
      if (credential.app?.slug === "zapier") {
        await prisma.apiKey.deleteMany({
          where: {
            userId: ctx.user.id,
            appId: "zapier",
          },
        });
        await prisma.webhook.deleteMany({
          where: {
            userId: ctx.user.id,
            appId: "zapier",
          },
        });
        const bookingsWithScheduledJobs = await prisma.booking.findMany({
          where: {
            userId: ctx.user.id,
            scheduledJobs: {
              isEmpty: false,
            },
          },
        });
        for (const booking of bookingsWithScheduledJobs) {
          cancelScheduledJobs(booking, credential.appId);
        }
      }

      // Validated that credential is user's above
      await prisma.credential.delete({
        where: {
          id: id,
        },
      });
      // Revalidate user calendar cache.
      if (credential.app?.slug.includes("calendar")) {
        await fetch(`${WEBAPP_URL}/api/revalidate-calendar-cache/${ctx?.user?.username}`);
      }
    }),
  bookingUnconfirmedCount: authedProcedure.query(async ({ ctx }) => {
    const { prisma, user } = ctx;
    const count = await prisma.booking.count({
      where: {
        status: BookingStatus.PENDING,
        userId: user.id,
        endTime: { gt: new Date() },
      },
    });
    const recurringGrouping = await prisma.booking.groupBy({
      by: ["recurringEventId"],
      _count: {
        recurringEventId: true,
      },
      where: {
        recurringEventId: { not: { equals: null } },
        status: { equals: "PENDING" },
        userId: user.id,
        endTime: { gt: new Date() },
      },
    });
    return recurringGrouping.reduce((prev, current) => {
      // recurringEventId is the total number of recurring instances for a booking
      // we need to subtract all but one, to represent a single recurring booking
      return prev - (current._count?.recurringEventId - 1);
    }, count);
  }),
  getCalVideoRecordings: authedProcedure
    .input(
      z.object({
        roomName: z.string(),
      })
    )
    .query(async ({ input }) => {
      const { roomName } = input;

      try {
        const res = await getRecordingsOfCalVideoByRoomName(roomName);
        return res;
      } catch (err) {
        throw new TRPCError({
          code: "BAD_REQUEST",
        });
      }
    }),
  getDownloadLinkOfCalVideoRecordings: authedProcedure
    .input(
      z.object({
        recordingId: z.string(),
      })
    )
    .query(async ({ input, ctx }) => {
      const { recordingId } = input;
      const { session } = ctx;

      const isDownloadAllowed = IS_SELF_HOSTED || session.user.belongsToActiveTeam;

      if (!isDownloadAllowed) {
        throw new TRPCError({
          code: "FORBIDDEN",
        });
      }

      try {
        const res = await getDownloadLinkOfCalVideoByRecordingId(recordingId);
        return res;
      } catch (err) {
        throw new TRPCError({
          code: "BAD_REQUEST",
        });
      }
    }),
  getUsersDefaultConferencingApp: authedProcedure.query(async ({ ctx }) => {
    return userMetadata.parse(ctx.user.metadata)?.defaultConferencingApp;
  }),
  updateUserDefaultConferencingApp: authedProcedure
    .input(
      z.object({
        appSlug: z.string().optional(),
        appLink: z.string().optional(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const currentMetadata = userMetadata.parse(ctx.user.metadata);
      const credentials = ctx.user.credentials;
      const foundApp = getApps(credentials).filter((app) => app.slug === input.appSlug)[0];
      const appLocation = foundApp?.appData?.location;

      if (!foundApp || !appLocation)
        throw new TRPCError({ code: "BAD_REQUEST", message: "App not installed" });

      if (appLocation.linkType === "static" && !input.appLink) {
        throw new TRPCError({ code: "BAD_REQUEST", message: "App link is required" });
      }

      if (appLocation.linkType === "static" && appLocation.urlRegExp) {
        const validLink = z
          .string()
          .regex(new RegExp(appLocation.urlRegExp), "Invalid App Link")
          .parse(input.appLink);
        if (!validLink) {
          throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid app link" });
        }
      }

      await ctx.prisma.user.update({
        where: {
          id: ctx.user.id,
        },
        data: {
          metadata: {
            ...currentMetadata,
            defaultConferencingApp: {
              appSlug: input.appSlug,
              appLink: input.appLink,
            },
          },
        },
      });
      return input;
    }),
});

export const viewerRouter = mergeRouters(
  loggedInViewerRouter,
  router({
    loggedInViewerRouter,
    public: publicViewerRouter,
    auth: authRouter,
    deploymentSetup: deploymentSetupRouter,
    bookings: bookingsRouter,
    eventTypes: eventTypesRouter,
    availability: availabilityRouter,
    teams: viewerTeamsRouter,
    webhook: webhookRouter,
    apiKeys: apiKeysRouter,
    workflows: workflowsRouter,
    saml: ssoRouter,
    insights: insightsRouter,
    // NOTE: Add all app related routes in the bottom till the problem described in @calcom/app-store/trpc-routers.ts is solved.
    // After that there would just one merge call here for all the apps.
    appRoutingForms: app_RoutingForms,
    eth: ethRouter,
    features: featureFlagRouter,
    payments: paymentsRouter,
    appsRouter,
    users: userAdminRouter,
  })
);
