import { Prisma } from "@prisma/client";
import { v4 } from "uuid";
import { z } from "zod";

import { WEBHOOK_TRIGGER_EVENTS } from "@calcom/features/webhooks/lib/constants";
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { getTranslation } from "@calcom/lib/server/i18n";

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

import { router, authedProcedure } from "../../trpc";

// Common data for all endpoints under webhook
const webhookIdAndEventTypeIdSchema = z.object({
  // Webhook ID
  id: z.string().optional(),
  // Event type ID
  eventTypeId: z.number().optional(),
});

const webhookProcedure = authedProcedure.use(async ({ ctx, rawInput, next }) => {
  // Endpoints that just read the logged in user's data - like 'list' don't necessary have any input
  if (!rawInput) {
    return next();
  }
  const webhookIdAndEventTypeId = webhookIdAndEventTypeIdSchema.safeParse(rawInput);
  if (!webhookIdAndEventTypeId.success) {
    throw new TRPCError({ code: "PARSE_ERROR" });
  }
  const { eventTypeId, id } = webhookIdAndEventTypeId.data;

  // A webhook is either linked to Event Type or to a user.
  if (eventTypeId) {
    const team = await ctx.prisma.team.findFirst({
      where: {
        eventTypes: {
          some: {
            id: eventTypeId,
          },
        },
      },
      include: {
        members: true,
      },
    });

    // Team should be available and the user should be a member of the team
    if (!team?.members.some((membership) => membership.userId === ctx.user.id)) {
      throw new TRPCError({
        code: "UNAUTHORIZED",
      });
    }
  } else if (id) {
    const authorizedHook = await ctx.prisma.webhook.findFirst({
      where: {
        id: id,
        userId: ctx.user.id,
      },
    });
    if (!authorizedHook) {
      throw new TRPCError({
        code: "UNAUTHORIZED",
      });
    }
  }
  return next();
});

export const webhookRouter = router({
  list: webhookProcedure
    .input(
      z
        .object({
          eventTypeId: z.number().optional(),
          appId: z.string().optional(),
        })
        .optional()
    )
    .query(async ({ ctx, input }) => {
      const where: Prisma.WebhookWhereInput = {
        /* Don't mixup zapier webhooks with normal ones */
        AND: [{ appId: !input?.appId ? null : input.appId }],
      };
      if (Array.isArray(where.AND)) {
        if (input?.eventTypeId) {
          where.AND?.push({ eventTypeId: input.eventTypeId });
        } else {
          where.AND?.push({ userId: ctx.user.id });
        }
      }

      return await ctx.prisma.webhook.findMany({
        where,
      });
    }),
  get: webhookProcedure
    .input(
      z.object({
        webhookId: z.string().optional(),
      })
    )
    .query(async ({ ctx, input }) => {
      return await ctx.prisma.webhook.findUniqueOrThrow({
        where: {
          id: input.webhookId,
        },
        select: {
          id: true,
          subscriberUrl: true,
          payloadTemplate: true,
          active: true,
          eventTriggers: true,
          secret: true,
        },
      });
    }),
  create: webhookProcedure
    .input(
      z.object({
        subscriberUrl: z.string().url(),
        eventTriggers: z.enum(WEBHOOK_TRIGGER_EVENTS).array(),
        active: z.boolean(),
        payloadTemplate: z.string().nullable(),
        eventTypeId: z.number().optional(),
        appId: z.string().optional().nullable(),
        secret: z.string().optional().nullable(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      if (input.eventTypeId) {
        return await ctx.prisma.webhook.create({
          data: {
            id: v4(),
            ...input,
          },
        });
      }

      return await ctx.prisma.webhook.create({
        data: {
          id: v4(),
          userId: ctx.user.id,
          ...input,
        },
      });
    }),
  edit: webhookProcedure
    .input(
      z.object({
        id: z.string(),
        subscriberUrl: z.string().url().optional(),
        eventTriggers: z.enum(WEBHOOK_TRIGGER_EVENTS).array().optional(),
        active: z.boolean().optional(),
        payloadTemplate: z.string().nullable(),
        eventTypeId: z.number().optional(),
        appId: z.string().optional().nullable(),
        secret: z.string().optional().nullable(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const { id, ...data } = input;
      const webhook = input.eventTypeId
        ? await ctx.prisma.webhook.findFirst({
            where: {
              eventTypeId: input.eventTypeId,
              id,
            },
          })
        : await ctx.prisma.webhook.findFirst({
            where: {
              userId: ctx.user.id,
              id,
            },
          });
      if (!webhook) {
        // user does not own this webhook
        // team event doesn't own this webhook
        return null;
      }
      return await ctx.prisma.webhook.update({
        where: {
          id,
        },
        data,
      });
    }),
  delete: webhookProcedure
    .input(
      z.object({
        id: z.string(),
        eventTypeId: z.number().optional(),
      })
    )
    .mutation(async ({ ctx, input }) => {
      const { id } = input;
      input.eventTypeId
        ? await ctx.prisma.eventType.update({
            where: {
              id: input.eventTypeId,
            },
            data: {
              webhooks: {
                delete: {
                  id,
                },
              },
            },
          })
        : await ctx.prisma.user.update({
            where: {
              id: ctx.user.id,
            },
            data: {
              webhooks: {
                delete: {
                  id,
                },
              },
            },
          });
      return {
        id,
      };
    }),
  testTrigger: webhookProcedure
    .input(
      z.object({
        url: z.string().url(),
        type: z.string(),
        payloadTemplate: z.string().optional().nullable(),
      })
    )
    .mutation(async ({ input }) => {
      const { url, type, payloadTemplate = null } = input;
      const translation = await getTranslation("en", "common");
      const language = {
        locale: "en",
        translate: translation,
      };

      const data = {
        type: "Test",
        title: "Test trigger event",
        description: "",
        startTime: new Date().toISOString(),
        endTime: new Date().toISOString(),
        attendees: [
          {
            email: "jdoe@example.com",
            name: "John Doe",
            timeZone: "Europe/London",
            language,
          },
        ],
        organizer: {
          name: "Cal",
          email: "no-reply@cal.com",
          timeZone: "Europe/London",
          language,
        },
      };

      try {
        const webhook = { subscriberUrl: url, payloadTemplate, appId: null, secret: null };
        return await sendPayload(null, type, new Date().toISOString(), webhook, data);
      } catch (_err) {
        const error = getErrorFromUnknown(_err);
        return {
          ok: false,
          status: 500,
          message: error.message,
        };
      }
    }),
});
