// eslint-disable-next-line no-restricted-imports
import { cloneDeep } from "lodash";

import { OrganizerDefaultConferencingAppType, getLocationValueForDB } from "@calcom/app-store/locations";
import { getEventName } from "@calcom/core/event";
import dayjs from "@calcom/dayjs";
import {
  sendRoundRobinCancelledEmailsAndSMS,
  sendRoundRobinScheduledEmailsAndSMS,
  sendRoundRobinUpdatedEmailsAndSMS,
} from "@calcom/emails";
import getBookingResponsesSchema from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import { ensureAvailableUsers } from "@calcom/features/bookings/lib/handleNewBooking/ensureAvailableUsers";
import { getEventTypesFromDB } from "@calcom/features/bookings/lib/handleNewBooking/getEventTypesFromDB";
import type { IsFixedAwareUser } from "@calcom/features/bookings/lib/handleNewBooking/types";
import {
  scheduleEmailReminder,
  deleteScheduledEmailReminder,
} from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager";
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
import { isPrismaObjOrUndefined } from "@calcom/lib";
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
import { SENDER_NAME } from "@calcom/lib/constants";
import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server";
import logger from "@calcom/lib/logger";
import { getLuckyUser } from "@calcom/lib/server";
import { getTranslation } from "@calcom/lib/server/i18n";
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import { prisma } from "@calcom/prisma";
import { WorkflowActions, WorkflowMethods, WorkflowTriggerEvents } from "@calcom/prisma/enums";
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
import type { EventTypeMetadata, PlatformClientParams } from "@calcom/prisma/zod-utils";
import type { CalendarEvent } from "@calcom/types/Calendar";

import { handleRescheduleEventManager } from "./handleRescheduleEventManager";
import { bookingSelect } from "./utils/bookingSelect";
import { getDestinationCalendar } from "./utils/getDestinationCalendar";
import { getTeamMembers } from "./utils/getTeamMembers";

export const roundRobinReassignment = async ({
  bookingId,
  orgId,
  emailsEnabled = true,
  platformClientParams,
}: {
  bookingId: number;
  orgId: number | null;
  emailsEnabled?: boolean;
  platformClientParams?: PlatformClientParams;
}) => {
  const roundRobinReassignLogger = logger.getSubLogger({
    prefix: ["roundRobinReassign", `${bookingId}`],
  });

  let booking = await prisma.booking.findUnique({
    where: {
      id: bookingId,
    },
    select: bookingSelect,
  });

  if (!booking) {
    logger.error(`Booking ${bookingId} not found`);
    throw new Error("Booking not found");
  }

  if (!booking.user) {
    logger.error(`No user associated with booking ${bookingId}`);
    throw new Error("Booking not found");
  }

  const eventTypeId = booking.eventTypeId;

  if (!eventTypeId) {
    logger.error(`Booking ${bookingId} does not have an event type id`);
    throw new Error("Event type not found");
  }

  const eventType = await getEventTypesFromDB(eventTypeId);

  if (!eventType) {
    logger.error(`Event type ${eventTypeId} not found`);
    throw new Error("Event type not found");
  }

  eventType.hosts = eventType.hosts.length
    ? eventType.hosts
    : eventType.users.map((user) => ({
        user,
        isFixed: false,
        priority: 2,
        weight: 100,
        schedule: null,
        createdAt: new Date(0), // use earliest possible date as fallback
      }));

  const roundRobinHosts = eventType.hosts.filter((host) => !host.isFixed);

  const originalOrganizer = booking.user;

  const attendeeEmailsSet = new Set(booking.attendees.map((attendee) => attendee.email));

  // Find the current round robin host assigned
  const previousRRHost = (() => {
    for (const host of roundRobinHosts) {
      if (host.user.id === booking.userId) {
        return host.user;
      }
      if (attendeeEmailsSet.has(host.user.email)) {
        return host.user;
      }
    }
  })();

  const previousRRHostT = await getTranslation(previousRRHost?.locale || "en", "common");

  // Filter out the current attendees of the booking from the event type
  const availableEventTypeUsers = eventType.hosts.reduce((availableUsers, host) => {
    if (!attendeeEmailsSet.has(host.user.email) && host.user.email !== originalOrganizer.email) {
      availableUsers.push({ ...host.user, isFixed: host.isFixed, priority: host?.priority ?? 2 });
    }
    return availableUsers;
  }, [] as IsFixedAwareUser[]);

  const availableUsers = await ensureAvailableUsers(
    { ...eventType, users: availableEventTypeUsers },
    {
      dateFrom: dayjs(booking.startTime).format(),
      dateTo: dayjs(booking.endTime).format(),
      timeZone: eventType.timeZone || originalOrganizer.timeZone,
    },
    roundRobinReassignLogger
  );

  const reassignedRRHost = await getLuckyUser({
    availableUsers,
    eventType,
    allRRHosts: eventType.hosts.filter((host) => !host.isFixed), // todo: only use hosts from virtual queue
    routingFormResponse: null,
  });

  const hasOrganizerChanged = !previousRRHost || booking.userId === previousRRHost?.id;
  const organizer = hasOrganizerChanged ? reassignedRRHost : booking.user;
  const organizerT = await getTranslation(organizer?.locale || "en", "common");

  const currentBookingTitle = booking.title;
  let newBookingTitle = currentBookingTitle;

  const reassignedRRHostT = await getTranslation(reassignedRRHost.locale || "en", "common");

  const teamMembers = await getTeamMembers({
    eventTypeHosts: eventType.hosts,
    attendees: booking.attendees,
    organizer: organizer,
    previousHost: previousRRHost || null,
    reassignedHost: reassignedRRHost,
  });

  const attendeePromises = [];
  for (const attendee of booking.attendees) {
    if (
      attendee.email === reassignedRRHost.email ||
      attendee.email === previousRRHost?.email ||
      teamMembers.some((member) => member.email === attendee.email)
    ) {
      continue;
    }

    attendeePromises.push(
      getTranslation(attendee.locale ?? "en", "common").then((tAttendee) => ({
        email: attendee.email,
        name: attendee.name,
        timeZone: attendee.timeZone,
        language: { translate: tAttendee, locale: attendee.locale ?? "en" },
        phoneNumber: attendee.phoneNumber || undefined,
      }))
    );
  }

  const attendeeList = await Promise.all(attendeePromises);
  let bookingLocation = booking.location;

  if (hasOrganizerChanged) {
    const bookingResponses = booking.responses;

    const responseSchema = getBookingResponsesSchema({
      bookingFields: eventType.bookingFields,
      view: "reschedule",
    });

    const responseSafeParse = await responseSchema.safeParseAsync(bookingResponses);

    const responses = responseSafeParse.success ? responseSafeParse.data : undefined;

    if (eventType.locations.some((location) => location.type === OrganizerDefaultConferencingAppType)) {
      const organizerMetadataSafeParse = userMetadataSchema.safeParse(reassignedRRHost.metadata);

      const defaultLocationUrl = organizerMetadataSafeParse.success
        ? organizerMetadataSafeParse?.data?.defaultConferencingApp?.appLink
        : undefined;

      const currentBookingLocation = booking.location || "integrations:daily";

      bookingLocation =
        defaultLocationUrl ||
        getLocationValueForDB(currentBookingLocation, eventType.locations).bookingLocation;
    }

    const eventNameObject = {
      attendeeName: responses?.name || "Nameless",
      eventType: eventType.title,
      eventName: eventType.eventName,
      // we send on behalf of team if >1 round robin attendee | collective
      teamName: teamMembers.length > 1 ? eventType.team?.name : null,
      // TODO: Can we have an unnamed organizer? If not, I would really like to throw an error here.
      host: organizer.name || "Nameless",
      location: bookingLocation || "integrations:daily",
      bookingFields: { ...responses },
      eventDuration: eventType.length,
      t: organizerT,
    };

    newBookingTitle = getEventName(eventNameObject);

    booking = await prisma.booking.update({
      where: {
        id: bookingId,
      },
      data: {
        userId: reassignedRRHost.id,
        title: newBookingTitle,
      },
      select: bookingSelect,
    });
  } else {
    const previousRRHostAttendee = booking.attendees.find(
      (attendee) => attendee.email === previousRRHost.email
    );
    await prisma.attendee.update({
      where: {
        id: previousRRHostAttendee!.id,
      },
      data: {
        name: reassignedRRHost.name || "",
        email: reassignedRRHost.email,
        timeZone: reassignedRRHost.timeZone,
        locale: reassignedRRHost.locale,
      },
    });
  }

  const destinationCalendar = await getDestinationCalendar({
    eventType,
    booking,
    newUserId: reassignedRRHost.id,
    hasOrganizerChanged,
  });

  // If changed owner, also change destination calendar
  const previousHostDestinationCalendar = hasOrganizerChanged
    ? await prisma.destinationCalendar.findFirst({
        where: {
          userId: originalOrganizer.id,
        },
      })
    : null;

  const evt: CalendarEvent = {
    organizer: {
      name: organizer.name || "",
      email: organizer.email,
      language: {
        locale: organizer.locale || "en",
        translate: organizerT,
      },
      timeZone: organizer.timeZone,
      timeFormat: getTimeFormatStringFromUserTimeFormat(organizer.timeFormat),
    },
    startTime: dayjs(booking.startTime).utc().format(),
    endTime: dayjs(booking.endTime).utc().format(),
    type: eventType.slug,
    title: newBookingTitle,
    description: eventType.description,
    attendees: attendeeList,
    uid: booking.uid,
    destinationCalendar,
    team: {
      members: teamMembers,
      name: eventType.team?.name || "",
      id: eventType.team?.id || 0,
    },
    customInputs: isPrismaObjOrUndefined(booking.customInputs),
    ...getCalEventResponses({
      bookingFields: eventType?.bookingFields ?? null,
      booking,
    }),
    location: bookingLocation,
    ...(platformClientParams ? platformClientParams : {}),
  };

  const credentials = await prisma.credential.findMany({
    where: {
      userId: organizer.id,
    },
    include: {
      user: {
        select: {
          email: true,
        },
      },
    },
  });

  const { evtWithAdditionalInfo } = await handleRescheduleEventManager({
    evt,
    rescheduleUid: booking.uid,
    newBookingId: undefined,
    changedOrganizer: hasOrganizerChanged,
    previousHostDestinationCalendar: previousHostDestinationCalendar ? [previousHostDestinationCalendar] : [],
    initParams: {
      user: { ...organizer, credentials: [...credentials] },
    },
    bookingId,
    bookingLocation,
    bookingICalUID: booking.iCalUID,
    bookingMetadata: booking.metadata,
  });

  const { cancellationReason, ...evtWithoutCancellationReason } = evtWithAdditionalInfo;

  // Send to new RR host
  if (emailsEnabled) {
    await sendRoundRobinScheduledEmailsAndSMS({
      calEvent: evtWithoutCancellationReason,
      members: [
        {
          ...reassignedRRHost,
          name: reassignedRRHost.name || "",
          username: reassignedRRHost.username || "",
          timeFormat: getTimeFormatStringFromUserTimeFormat(reassignedRRHost.timeFormat),
          language: { translate: reassignedRRHostT, locale: reassignedRRHost.locale || "en" },
        },
      ],
    });
  }

  if (previousRRHost) {
    // Send to cancelled RR host
    // First we need to replace the new RR host with the old RR host in the evt object
    const cancelledRRHostEvt = cloneDeep(evtWithAdditionalInfo);
    cancelledRRHostEvt.title = currentBookingTitle;
    if (hasOrganizerChanged) {
      cancelledRRHostEvt.organizer = {
        name: previousRRHost.name || "",
        email: previousRRHost.email,
        language: {
          locale: previousRRHost.locale || "en",
          translate: previousRRHostT,
        },
        timeZone: previousRRHost.timeZone,
        timeFormat: getTimeFormatStringFromUserTimeFormat(previousRRHost.timeFormat),
      };
    } else if (cancelledRRHostEvt.team) {
      // Filter out the new RR host from attendees and add the old RR host
      const newMembersArray = cancelledRRHostEvt.team?.members || [];
      cancelledRRHostEvt.team.members = newMembersArray.filter(
        (member) => member.email !== reassignedRRHost.email
      );
      cancelledRRHostEvt.team.members.unshift({
        id: previousRRHost.id,
        email: previousRRHost.email,
        name: previousRRHost.name || "",
        timeZone: previousRRHost.timeZone,
        language: { translate: previousRRHostT, locale: previousRRHost.locale || "en" },
      });
    }

    if (emailsEnabled) {
      await sendRoundRobinCancelledEmailsAndSMS(
        cancelledRRHostEvt,
        [
          {
            ...previousRRHost,
            name: previousRRHost.name || "",
            username: previousRRHost.username || "",
            timeFormat: getTimeFormatStringFromUserTimeFormat(previousRRHost.timeFormat),
            language: { translate: previousRRHostT, locale: previousRRHost.locale || "en" },
          },
        ],
        eventType?.metadata as EventTypeMetadata,
        { name: reassignedRRHost.name, email: reassignedRRHost.email }
      );
    }
  }

  // Handle changing workflows with organizer
  if (hasOrganizerChanged) {
    if (emailsEnabled && dayjs(evt.startTime).isAfter(dayjs())) {
      // send email with event updates to attendees
      await sendRoundRobinUpdatedEmailsAndSMS({
        calEvent: evtWithoutCancellationReason,
      });
    }

    const scheduledWorkflowReminders = await prisma.workflowReminder.findMany({
      where: {
        bookingUid: booking.uid,
        method: WorkflowMethods.EMAIL,
        scheduled: true,
        OR: [{ cancelled: false }, { cancelled: null }],
        workflowStep: {
          workflow: {
            trigger: {
              in: [
                WorkflowTriggerEvents.BEFORE_EVENT,
                WorkflowTriggerEvents.NEW_EVENT,
                WorkflowTriggerEvents.AFTER_EVENT,
              ],
            },
          },
        },
      },
      select: {
        id: true,
        referenceId: true,
        workflowStep: {
          select: {
            id: true,
            template: true,
            workflow: {
              select: {
                trigger: true,
                time: true,
                timeUnit: true,
              },
            },
            emailSubject: true,
            reminderBody: true,
            sender: true,
            includeCalendarEvent: true,
          },
        },
      },
    });

    const workflowEventMetadata = { videoCallUrl: getVideoCallUrlFromCalEvent(evtWithAdditionalInfo) };

    const bookerUrl = await getBookerBaseUrl(orgId);

    for (const workflowReminder of scheduledWorkflowReminders) {
      const workflowStep = workflowReminder?.workflowStep;
      const workflow = workflowStep?.workflow;

      if (workflowStep && workflow) {
        await scheduleEmailReminder({
          evt: {
            ...evtWithAdditionalInfo,
            metadata: workflowEventMetadata,
            eventType,
            bookerUrl,
          },
          action: WorkflowActions.EMAIL_HOST,
          triggerEvent: workflow.trigger,
          timeSpan: {
            time: workflow.time,
            timeUnit: workflow.timeUnit,
          },
          sendTo: reassignedRRHost.email,
          template: workflowStep.template,
          emailSubject: workflowStep.emailSubject || undefined,
          emailBody: workflowStep.reminderBody || undefined,
          sender: workflowStep.sender || SENDER_NAME,
          hideBranding: true,
          includeCalendarEvent: workflowStep.includeCalendarEvent,
          workflowStepId: workflowStep.id,
        });
      }

      await deleteScheduledEmailReminder(workflowReminder.id, workflowReminder.referenceId);
    }
    // Send new event workflows to new organizer
    const newEventWorkflows = await prisma.workflow.findMany({
      where: {
        trigger: WorkflowTriggerEvents.NEW_EVENT,
        OR: [
          {
            isActiveOnAll: true,
            teamId: eventType?.teamId,
          },
          {
            activeOn: {
              some: {
                eventTypeId: eventTypeId,
              },
            },
          },
          ...(eventType?.teamId
            ? [
                {
                  activeOnTeams: {
                    some: {
                      teamId: eventType.teamId,
                    },
                  },
                },
              ]
            : []),
          ...(eventType?.team?.parentId
            ? [
                {
                  isActiveOnAll: true,
                  teamId: eventType.team.parentId,
                },
              ]
            : []),
        ],
      },
      include: {
        steps: {
          where: {
            action: WorkflowActions.EMAIL_HOST,
          },
        },
      },
    });

    await scheduleWorkflowReminders({
      workflows: newEventWorkflows,
      smsReminderNumber: null,
      calendarEvent: {
        ...evtWithAdditionalInfo,
        metadata: workflowEventMetadata,
        eventType: { slug: eventType.slug },
        bookerUrl,
      },
      hideBranding: !!eventType?.owner?.hideBranding,
    });
  }

  return {
    bookingId,
    reassignedTo: {
      id: reassignedRRHost.id,
      name: reassignedRRHost.name,
      email: reassignedRRHost.email,
    },
  };
};

export default roundRobinReassignment;
