import DatabaseConfig from "../DatabaseConfig";
import CreateBy from "../Types/Database/CreateBy";
import { OnCreate, OnUpdate } from "../Types/Database/Hooks";
import UpdateBy from "../Types/Database/UpdateBy";
import CookieUtil from "../Utils/Cookie";
import { ExpressRequest } from "../Utils/Express";
import JSONWebToken from "../Utils/JsonWebToken";
import logger from "../Utils/Logger";
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
import DatabaseService from "./DatabaseService";
import MonitorStatusService from "./MonitorStatusService";
import ProjectService, { CurrentPlan } from "./ProjectService";
import StatusPageDomainService from "./StatusPageDomainService";
import StatusPageOwnerTeamService from "./StatusPageOwnerTeamService";
import StatusPageOwnerUserService from "./StatusPageOwnerUserService";
import TeamMemberService from "./TeamMemberService";
import Hostname from "../../Types/API/Hostname";
import Protocol from "../../Types/API/Protocol";
import URL from "../../Types/API/URL";
import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
import { Green } from "../../Types/BrandColors";
import LIMIT_MAX, { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
import BadDataException from "../../Types/Exception/BadDataException";
import JSONWebTokenData from "../../Types/JsonWebTokenData";
import ObjectID from "../../Types/ObjectID";
import PositiveNumber from "../../Types/PositiveNumber";
import Typeof from "../../Types/Typeof";
import MonitorStatus from "../../Models/DatabaseModels/MonitorStatus";
import StatusPage from "../../Models/DatabaseModels/StatusPage";
import StatusPageDomain from "../../Models/DatabaseModels/StatusPageDomain";
import StatusPageOwnerTeam from "../../Models/DatabaseModels/StatusPageOwnerTeam";
import StatusPageOwnerUser from "../../Models/DatabaseModels/StatusPageOwnerUser";
import User from "../../Models/DatabaseModels/User";
import {
  AllowedStatusPageCountInFreePlan,
  IsBillingEnabled,
} from "../EnvironmentConfig";
import { PlanType } from "../../Types/Billing/SubscriptionPlan";
import Recurring from "../../Types/Events/Recurring";
import Email from "../../Types/Email";
import StatusPageSubscriberService from "./StatusPageSubscriberService";
import StatusPageSubscriber from "../../Models/DatabaseModels/StatusPageSubscriber";
import MailService from "./MailService";
import EmailTemplateType from "../../Types/Email/EmailTemplateType";
import { FileRoute } from "../../ServiceRoute";
import ProjectSMTPConfigService from "./ProjectSmtpConfigService";
import StatusPageResource from "../../Models/DatabaseModels/StatusPageResource";
import StatusPageResourceService from "./StatusPageResourceService";
import Dictionary from "../../Types/Dictionary";
import MonitorGroupResource from "../../Models/DatabaseModels/MonitorGroupResource";
import MonitorGroupResourceService from "./MonitorGroupResourceService";
import QueryHelper from "../Types/Database/QueryHelper";
import OneUptimeDate from "../../Types/Date";
import IncidentService from "./IncidentService";
import MonitorStatusTimeline from "../../Models/DatabaseModels/MonitorStatusTimeline";
import MonitorStatusTimelineService from "./MonitorStatusTimelineService";
import SortOrder from "../../Types/BaseDatabase/SortOrder";
import UptimeUtil from "../../Utils/Uptime/UptimeUtil";
import UptimePrecision from "../../Types/StatusPage/UptimePrecision";
import IP from "../../Types/IP/IP";
import NotAuthenticatedException from "../../Types/Exception/NotAuthenticatedException";
import ForbiddenException from "../../Types/Exception/ForbiddenException";
import CommonAPI from "../API/CommonAPI";

export interface StatusPageReportItem {
  resourceName: string;
  totalIncidentCount: number;
  uptimePercent: number;
  uptimePercentAsString: string;
  downtimeInHoursAndMinutes: string;
}

export interface StatusPageReport {
  reportDates: string; // start date and end date in string. e.g. "01 July 2021 - 14 July 2021"
  totalResources: number;
  totalIncidents: number;
  averageUptimePercent: string;
  totalDowntimeInHoursAndMinutes: string;
  resources: Array<StatusPageReportItem>;
}

export class Service extends DatabaseService<StatusPage> {
  public constructor() {
    super(StatusPage);
  }

  public static getDefaultEmailFooterText(): string {
    return "This is an automated email sent to you because you are subscribed to this Status Page.";
  }

  public static getSubscriberEmailFooterText(statusPage: StatusPage): string {
    if (
      statusPage.enableCustomSubscriberEmailNotificationFooterText &&
      statusPage.subscriberEmailNotificationFooterText
    ) {
      return statusPage.subscriberEmailNotificationFooterText;
    }
    return this.getDefaultEmailFooterText();
  }

  @CaptureSpan()
  protected override async onBeforeCreate(
    createBy: CreateBy<StatusPage>,
  ): Promise<OnCreate<StatusPage>> {
    if (!createBy.data.projectId) {
      throw new BadDataException("projectId is required");
    }

    // if the project is on the free plan, then only allow 1 status page.
    if (IsBillingEnabled) {
      const currentPlan: CurrentPlan = await ProjectService.getCurrentPlan(
        createBy.data.projectId,
      );

      if (currentPlan.isSubscriptionUnpaid) {
        throw new BadDataException(
          "Your subscription is unpaid. Please update your payment method and pay all the outstanding invoices to add more status pages.",
        );
      }

      if (currentPlan.plan === PlanType.Free) {
        const statusPageCount: PositiveNumber = await this.countBy({
          query: {
            projectId: createBy.data.projectId,
          },
          props: {
            isRoot: true,
          },
        });

        if (statusPageCount.toNumber() >= AllowedStatusPageCountInFreePlan) {
          throw new BadDataException(
            `You have reached the maximum allowed status page limit for the free plan. Please upgrade your plan to add more status pages.`,
          );
        }
      }
    }

    if (
      !createBy.data.downtimeMonitorStatuses ||
      createBy.data.downtimeMonitorStatuses.length === 0
    ) {
      const monitorStatuses: Array<MonitorStatus> =
        await MonitorStatusService.findBy({
          query: {
            projectId: createBy.data.projectId,
          },
          select: {
            _id: true,
            isOperationalState: true,
          },
          props: {
            isRoot: true,
          },
          skip: 0,
          limit: LIMIT_PER_PROJECT,
        });

      const getNonOperationStatuses: Array<MonitorStatus> =
        monitorStatuses.filter((monitorStatus: MonitorStatus) => {
          return !monitorStatus.isOperationalState;
        });

      createBy.data.downtimeMonitorStatuses = getNonOperationStatuses;
    }

    if (!createBy.data.defaultBarColor) {
      createBy.data.defaultBarColor = Green;
    }

    // For new status pages, set enableCustomSubscriberEmailNotificationFooterText to false by default
    // and provide a default custom footer text only if not provided
    if (
      createBy.data.enableCustomSubscriberEmailNotificationFooterText ===
      undefined
    ) {
      createBy.data.enableCustomSubscriberEmailNotificationFooterText = false;
    }

    if (!createBy.data.subscriberEmailNotificationFooterText) {
      createBy.data.subscriberEmailNotificationFooterText =
        "This is an automated email sent to you because you are subscribed to " +
        (createBy.data?.pageTitle || createBy.data?.name || "Status Page");
    }

    return {
      createBy,
      carryForward: null,
    };
  }

  @CaptureSpan()
  protected override async onCreateSuccess(
    onCreate: OnCreate<StatusPage>,
    createdItem: StatusPage,
  ): Promise<StatusPage> {
    // Execute owner assignment asynchronously
    if (
      createdItem.projectId &&
      createdItem.id &&
      onCreate.createBy.miscDataProps &&
      (onCreate.createBy.miscDataProps["ownerTeams"] ||
        onCreate.createBy.miscDataProps["ownerUsers"])
    ) {
      // Run owner assignment in background without blocking
      this.addOwners(
        createdItem.projectId!,
        createdItem.id!,
        (onCreate.createBy.miscDataProps!["ownerUsers"] as Array<ObjectID>) ||
          [],
        (onCreate.createBy.miscDataProps!["ownerTeams"] as Array<ObjectID>) ||
          [],
        false,
        onCreate.createBy.props,
      ).catch((error: Error) => {
        logger.error(`Error in StatusPageService owner assignment: ${error}`);
      });
    }

    return createdItem;
  }

  @CaptureSpan()
  public async findOwners(statusPageId: ObjectID): Promise<Array<User>> {
    if (!statusPageId) {
      throw new BadDataException("statusPageId is required");
    }

    const ownerUsers: Array<StatusPageOwnerUser> =
      await StatusPageOwnerUserService.findBy({
        query: {
          statusPageId: statusPageId,
        },
        select: {
          _id: true,
          user: {
            _id: true,
            email: true,
            name: true,
          },
        },
        props: {
          isRoot: true,
        },
        limit: LIMIT_PER_PROJECT,
        skip: 0,
      });

    const ownerTeams: Array<StatusPageOwnerTeam> =
      await StatusPageOwnerTeamService.findBy({
        query: {
          statusPageId: statusPageId,
        },
        select: {
          _id: true,
          teamId: true,
        },
        skip: 0,
        limit: LIMIT_PER_PROJECT,
        props: {
          isRoot: true,
        },
      });

    const users: Array<User> =
      ownerUsers.map((ownerUser: StatusPageOwnerUser) => {
        return ownerUser.user!;
      }) || [];

    if (ownerTeams.length > 0) {
      const teamIds: Array<ObjectID> =
        ownerTeams.map((ownerTeam: StatusPageOwnerTeam) => {
          return ownerTeam.teamId!;
        }) || [];

      const teamUsers: Array<User> =
        await TeamMemberService.getUsersInTeams(teamIds);

      for (const teamUser of teamUsers) {
        //check if the user is already added.
        const isUserAlreadyAdded: User | undefined = users.find(
          (user: User) => {
            return user.id!.toString() === teamUser.id!.toString();
          },
        );

        if (!isUserAlreadyAdded) {
          users.push(teamUser);
        }
      }
    }

    return users;
  }

  @CaptureSpan()
  public async addOwners(
    projectId: ObjectID,
    statusPageId: ObjectID,
    userIds: Array<ObjectID>,
    teamIds: Array<ObjectID>,
    notifyOwners: boolean,
    props: DatabaseCommonInteractionProps,
  ): Promise<void> {
    for (let teamId of teamIds) {
      if (typeof teamId === Typeof.String) {
        teamId = new ObjectID(teamId.toString());
      }

      const teamOwner: StatusPageOwnerTeam = new StatusPageOwnerTeam();
      teamOwner.statusPageId = statusPageId;
      teamOwner.projectId = projectId;
      teamOwner.teamId = teamId;
      teamOwner.isOwnerNotified = !notifyOwners;

      await StatusPageOwnerTeamService.create({
        data: teamOwner,
        props: props,
      });
    }

    for (let userId of userIds) {
      if (typeof userId === Typeof.String) {
        userId = new ObjectID(userId.toString());
      }
      const teamOwner: StatusPageOwnerUser = new StatusPageOwnerUser();
      teamOwner.statusPageId = statusPageId;
      teamOwner.projectId = projectId;
      teamOwner.userId = userId;
      teamOwner.isOwnerNotified = !notifyOwners;
      await StatusPageOwnerUserService.create({
        data: teamOwner,
        props: props,
      });
    }
  }

  @CaptureSpan()
  public async getStatusPageLinkInDashboard(
    projectId: ObjectID,
    statusPageId: ObjectID,
  ): Promise<URL> {
    const dahboardUrl: URL = await DatabaseConfig.getDashboardUrl();

    return URL.fromString(dahboardUrl.toString()).addRoute(
      `/${projectId.toString()}/status-pages/${statusPageId.toString()}`,
    );
  }

  @CaptureSpan()
  public async hasReadAccess(data: {
    statusPageId: ObjectID;
    req: ExpressRequest;
  }): Promise<{
    hasReadAccess: boolean;
    error?: NotAuthenticatedException | ForbiddenException;
  }> {
    const statusPageId: ObjectID = data.statusPageId;
    const req: ExpressRequest = data.req;

    const props: DatabaseCommonInteractionProps =
      await CommonAPI.getDatabaseCommonInteractionProps(req);
    try {
      // get status page by id.
      const statusPage: StatusPage | null = await this.findOneById({
        id: statusPageId,
        props: {
          isRoot: true,
        },
        select: {
          _id: true,
          isPublicStatusPage: true,
          ipWhitelist: true,
        },
      });

      if (statusPage?.ipWhitelist && statusPage.ipWhitelist.length > 0) {
        const ipWhitelist: Array<string> = statusPage.ipWhitelist?.split("\n");

        const ipAccessedFrom: string | undefined =
          req.headers["x-forwarded-for"]?.toString() ||
          req.headers["x-real-ip"]?.toString() ||
          req.socket.remoteAddress ||
          req.ip ||
          req.ips[0];

        if (!ipAccessedFrom) {
          logger.error("IP address not found in request.");
          return {
            hasReadAccess: false,
            error: new ForbiddenException(
              "Unable to verify IP address for status page access.",
            ),
          };
        }

        const isIPWhitelisted: boolean = IP.isInWhitelist({
          ips:
            ipAccessedFrom?.split(",").map((i: string) => {
              return i.trim();
            }) || [],
          whitelist: ipWhitelist,
        });

        if (!isIPWhitelisted) {
          logger.error(
            `IP address ${ipAccessedFrom} is not whitelisted for status page ${statusPageId.toString()}.`,
          );

          return {
            hasReadAccess: false,
            error: new ForbiddenException(
              `Your IP address ${ipAccessedFrom} is blocked from accessing this status page.`,
            ),
          };
        }
      }

      if (statusPage && statusPage.isPublicStatusPage) {
        return {
          hasReadAccess: true,
        };
      }

      // token decode.
      const token: string | undefined = CookieUtil.getCookieFromExpressRequest(
        req,
        CookieUtil.getUserTokenKey(statusPageId),
      );

      if (token) {
        try {
          const decoded: JSONWebTokenData = JSONWebToken.decode(
            token as string,
          );

          if (decoded.statusPageId?.toString() === statusPageId.toString()) {
            return {
              hasReadAccess: true,
            };
          }
        } catch (err) {
          logger.error(err);
        }
      }

      // if it does not have public access, check if this user has access.

      const items: Array<StatusPage> = await this.findBy({
        query: {
          _id: statusPageId.toString(),
        },
        select: {
          _id: true,
        },
        skip: 0,
        limit: 1,
        props: props,
      });

      if (items.length > 0) {
        return {
          hasReadAccess: true,
        };
      }
    } catch (err) {
      logger.error(err);
    }

    return {
      hasReadAccess: false,
      error: new NotAuthenticatedException(
        "You do not have access to this status page. Please login to view the status page.",
      ),
    };
  }

  @CaptureSpan()
  public async getMonitorStatusTimelineForStatusPage(data: {
    monitorIds: Array<ObjectID>;
    startDate: Date;
    endDate: Date;
  }): Promise<Array<MonitorStatusTimeline>> {
    const startDate: Date = data.startDate;
    const endDate: Date = data.endDate;

    let monitorStatusTimelines: Array<MonitorStatusTimeline> = [];

    if (data.monitorIds.length > 0) {
      monitorStatusTimelines = await MonitorStatusTimelineService.findBy({
        query: {
          monitorId: QueryHelper.any(data.monitorIds),
          endsAt: QueryHelper.inBetween(startDate, endDate),
        },
        select: {
          monitorId: true,
          createdAt: true,
          endsAt: true,
          startsAt: true,
          monitorStatus: {
            name: true,
            color: true,
            priority: true,
          } as any,
        },
        sort: {
          createdAt: SortOrder.Descending,
        },
        skip: 0,
        limit: LIMIT_MAX, // This can be optimized.
        props: {
          isRoot: true,
        },
      });

      monitorStatusTimelines = monitorStatusTimelines.concat(
        await MonitorStatusTimelineService.findBy({
          query: {
            monitorId: QueryHelper.any(data.monitorIds),
            endsAt: QueryHelper.isNull(),
          },
          select: {
            monitorId: true,
            createdAt: true,
            endsAt: true,
            startsAt: true,
            monitorStatus: {
              name: true,
              color: true,
              priority: true,
            } as any,
          },
          sort: {
            createdAt: SortOrder.Descending,
          },
          skip: 0,
          limit: LIMIT_MAX, // This can be optimized.
          props: {
            isRoot: true,
          },
        }),
      );

      // sort monitorStatusTimelines by createdAt.
      monitorStatusTimelines = monitorStatusTimelines.sort(
        (a: MonitorStatusTimeline, b: MonitorStatusTimeline) => {
          if (!a.createdAt || !b.createdAt) {
            return 0;
          }

          return b.createdAt!.getTime() - a.createdAt!.getTime();
        },
      );
    }

    return monitorStatusTimelines;
  }

  @CaptureSpan()
  public async getStatusPageURL(statusPageId: ObjectID): Promise<string> {
    const domain: StatusPageDomain | null =
      await StatusPageDomainService.findOneBy({
        query: {
          statusPageId: statusPageId,
          isSslProvisioned: true,
        },
        select: {
          fullDomain: true,
        },
        props: {
          isRoot: true,
          ignoreHooks: true,
        },
      });

    let statusPageURL: string = domain?.fullDomain
      ? `https://${domain.fullDomain}`
      : "";

    if (!statusPageURL) {
      const host: Hostname = await DatabaseConfig.getHost();

      const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();

      // 'https://local.oneuptime.com/status-page/40092fb5-cc33-4995-b532-b4e49c441c98'
      statusPageURL = new URL(httpProtocol, host)
        .addRoute("/status-page/" + statusPageId.toString())
        .toString();
    }

    return statusPageURL;
  }

  @CaptureSpan()
  public async getStatusPageFirstURL(statusPageId: ObjectID): Promise<string> {
    const domains: Array<StatusPageDomain> =
      await StatusPageDomainService.findBy({
        query: {
          statusPageId: statusPageId,
          isSslProvisioned: true,
        },
        select: {
          fullDomain: true,
        },
        skip: 0,
        limit: LIMIT_PER_PROJECT,
        props: {
          isRoot: true,
          ignoreHooks: true,
        },
      });

    let statusPageURL: string = "";

    if (domains.length === 0) {
      const host: Hostname = await DatabaseConfig.getHost();

      const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();

      // 'https://local.oneuptime.com/status-page/40092fb5-cc33-4995-b532-b4e49c441c98'
      statusPageURL = new URL(httpProtocol, host)
        .addRoute("/status-page/" + statusPageId.toString())
        .toString();
    } else {
      statusPageURL = domains[0]?.fullDomain || "";
    }

    return statusPageURL;
  }

  @CaptureSpan()
  protected override async onBeforeUpdate(
    updateBy: UpdateBy<StatusPage>,
  ): Promise<OnUpdate<StatusPage>> {
    // is enabling SMS subscribers.

    if (updateBy.data.enableSmsSubscribers) {
      const statusPagesToBeUpdated: Array<StatusPage> = await this.findBy({
        query: updateBy.query,
        select: {
          _id: true,
          projectId: true,
        },
        props: {
          isRoot: true,
        },
        skip: 0,
        limit: LIMIT_PER_PROJECT,
      });

      for (const statusPage of statusPagesToBeUpdated) {
        const isSMSEnabled: boolean =
          await ProjectService.isSMSNotificationsEnabled(statusPage.projectId!);

        if (!isSMSEnabled) {
          throw new BadDataException(
            "SMS notifications are not enabled for this project. Please enable SMS notifications in the Project Settings > Notifications Settings.",
          );
        }
      }
    }

    if (
      updateBy.data.reportStartDateTime ||
      updateBy.data.reportRecurringInterval ||
      updateBy.data.sendNextReportBy
    ) {
      const statusPages: Array<StatusPage> = await this.findBy({
        query: updateBy.query,
        select: {
          _id: true,
          reportStartDateTime: true,
          reportRecurringInterval: true,
        },
        props: {
          isRoot: true,
        },
        skip: 0,
        limit: LIMIT_PER_PROJECT,
      });

      for (const statusPage of statusPages) {
        const rerportStartDate: Date | undefined =
          (updateBy.data.reportStartDateTime as Date) ||
          statusPage.reportStartDateTime;
        const reportRecurringInterval: Recurring | undefined =
          Recurring.fromJSON(
            (updateBy.data.reportRecurringInterval as Recurring) ||
              statusPage.reportRecurringInterval,
          );

        if (rerportStartDate && reportRecurringInterval) {
          const nextReportDate: Date = Recurring.getNextDate(
            rerportStartDate,
            reportRecurringInterval,
          );
          updateBy.data.sendNextReportBy = nextReportDate;
        }
      }
    }

    return {
      carryForward: null,
      updateBy: updateBy,
    };
  }

  @CaptureSpan()
  public async sendEmailReport(data: {
    statusPageId: ObjectID;
    email?: Email | undefined;
  }): Promise<void> {
    const host: Hostname = await DatabaseConfig.getHost();
    const httpProtocol: Protocol = await DatabaseConfig.getHttpProtocol();

    const statusPages: Array<StatusPage> =
      await StatusPageSubscriberService.getStatusPagesToSendNotification([
        data.statusPageId,
      ]);

    if (statusPages.length === 0) {
      throw new BadDataException("Status page not found");
    }

    const statuspage: StatusPage = statusPages[0]!;

    if (!statuspage.id) {
      throw new BadDataException("Status page not found");
    }

    const statusPageURL: string = await this.getStatusPageURL(statuspage.id);
    const statusPageName: string =
      statuspage.pageTitle || statuspage.name || "Status Page";

    const report: StatusPageReport = await this.getReportByStatusPage({
      statusPageId: statuspage.id!,
      historyDays: statuspage.reportDataInDays || 14,
    });

    type SendEmailFunction = (
      email: Email,
      unsubscribeUrl: URL | null,
    ) => Promise<void>;

    const sendEmail: SendEmailFunction = async (
      email: Email,
      unsubscribeUrl: URL | null,
    ): Promise<void> => {
      // send email here.

      MailService.sendMail(
        {
          toEmail: email,
          templateType: EmailTemplateType.StatusPageSubscriberReport,
          vars: {
            statusPageName: statusPageName,
            subscriberEmailNotificationFooterText:
              Service.getSubscriberEmailFooterText(statuspage),
            statusPageUrl: statusPageURL,
            hasResources: report.totalResources > 0 ? "true" : "false",
            report: report as any,
            logoUrl: statuspage.logoFileId
              ? new URL(httpProtocol, host)
                  .addRoute(FileRoute)
                  .addRoute("/image/" + statuspage.logoFileId)
                  .toString()
              : "",
            isPublicStatusPage: statuspage.isPublicStatusPage
              ? "true"
              : "false",

            unsubscribeUrl: unsubscribeUrl?.toString() || "",
          },
          subject: "[Report] " + statusPageName,
        },
        {
          mailServer: ProjectSMTPConfigService.toEmailServer(
            statuspage.smtpConfig,
          ),
          projectId: statuspage.projectId,
        },
      ).catch((err: Error) => {
        logger.error(err);
      });
    };

    if (data.email) {
      // force send to this email instead of sending to all subscribers.
      await sendEmail(data.email, null);
    }

    const subscribers: Array<StatusPageSubscriber> =
      await StatusPageSubscriberService.getSubscribersByStatusPage(
        statuspage.id!,
        {
          isRoot: true,
          ignoreHooks: true,
        },
      );

    for (const subscriber of subscribers) {
      try {
        if (!subscriber._id) {
          continue;
        }

        const shouldNotifySubscriber: boolean = !subscriber.isUnsubscribed;

        if (!shouldNotifySubscriber) {
          continue;
        }

        const unsubscribeUrl: string =
          StatusPageSubscriberService.getUnsubscribeLink(
            URL.fromString(statusPageURL),
            subscriber.id!,
          ).toString();

        if (subscriber.subscriberEmail) {
          await sendEmail(
            subscriber.subscriberEmail,
            URL.fromString(unsubscribeUrl),
          );
        }

        if (subscriber.subscriberPhone) {
          continue; // Cant send Status Page reports to SMS subscribers.
        }
      } catch (err) {
        logger.error(err);
      }
    }
  }

  @CaptureSpan()
  public async getReportByStatusPage(data: {
    statusPageId: ObjectID;
    historyDays: number;
  }): Promise<StatusPageReport> {
    const statusPage: StatusPage | null = await this.findOneById({
      id: data.statusPageId,
      props: {
        isRoot: true,
      },
      select: {
        downtimeMonitorStatuses: true,
      },
    });

    if (!statusPage) {
      throw new BadDataException("Status page not found");
    }

    const statusPageResources: StatusPageResource[] =
      await this.getStatusPageResources({
        statusPageId: data.statusPageId,
      });

    const numberOfDays: number = data.historyDays || 14;

    const endDate: Date = OneUptimeDate.getCurrentDate();
    const startDate: Date = OneUptimeDate.getSomeDaysAgo(numberOfDays);
    const startAndEndDate: string = `${numberOfDays} days (${OneUptimeDate.getDateAsLocalFormattedString(startDate, true)} - ${OneUptimeDate.getDateAsLocalFormattedString(endDate, true)})`;

    if (statusPageResources.length === 0) {
      return {
        reportDates: startAndEndDate,
        totalResources: 0,
        totalIncidents: 0,
        averageUptimePercent: "0%",
        totalDowntimeInHoursAndMinutes: "0",
        resources: [],
      };
    }

    const incidentCount: number = await this.getIncidentCountOnStatusPage({
      statusPageId: data.statusPageId,
      historyDays: data.historyDays,
    });

    const monitors: {
      monitorsOnStatusPage: Array<ObjectID>;
      monitorsInGroup: Dictionary<Array<ObjectID>>;
    } = await this.getMonitorIdsOnStatusPage({
      statusPageId: data.statusPageId,
    });

    const timeline: Array<MonitorStatusTimeline> =
      await this.getMonitorStatusTimelineForStatusPage({
        monitorIds: monitors.monitorsOnStatusPage,
        startDate: startDate,
        endDate: endDate,
      });

    const reportItems: Array<StatusPageReportItem> = [];

    for (const resource of statusPageResources) {
      // for each of these resource, calculate uptime percent.

      let monitorIdsForThisResource: Array<ObjectID> = [];

      if (resource.monitorId) {
        monitorIdsForThisResource.push(resource.monitorId);
      }

      if (resource.monitorGroupId) {
        const groupId: string = resource.monitorGroupId.toString();
        monitorIdsForThisResource = monitorIdsForThisResource.concat(
          monitors.monitorsInGroup[groupId] || [],
        );
      }

      const timelineForThisResource: Array<MonitorStatusTimeline> =
        timeline.filter((item: MonitorStatusTimeline) => {
          return monitorIdsForThisResource.find((id: ObjectID) => {
            return id.toString() === item.monitorId?.toString();
          });
        });

      const uptimePercent: number = UptimeUtil.calculateUptimePercentage(
        timelineForThisResource,
        resource.uptimePercentPrecision || UptimePrecision.TWO_DECIMAL,
        statusPage.downtimeMonitorStatuses!,
      );
      const downtime: {
        totalDowntimeInSeconds: number;
        totalSecondsInTimePeriod: number;
      } = UptimeUtil.getTotalDowntimeInSeconds(
        timelineForThisResource,
        statusPage.downtimeMonitorStatuses!,
      );

      const reportItem: StatusPageReportItem = {
        resourceName: resource.displayName || "",
        totalIncidentCount: await this.getIncidentCountByMonitorIds({
          monitorIds: monitorIdsForThisResource,
          historyDays: data.historyDays,
        }),
        uptimePercent: uptimePercent,
        uptimePercentAsString: `${uptimePercent}%`,
        downtimeInHoursAndMinutes:
          OneUptimeDate.convertMinutesToDaysHoursAndMinutes(
            Math.ceil(downtime.totalDowntimeInSeconds / 60),
          ),
      };

      reportItems.push(reportItem);
    }

    const avgUptimePercent: number =
      reportItems.reduce((acc: number, item: StatusPageReportItem) => {
        return acc + item.uptimePercent;
      }, 0) / reportItems.length;

    const avgUptimePercentString: string = avgUptimePercent.toFixed(2) + "%";

    const totalDowntimeInSeconds: {
      totalDowntimeInSeconds: number;
      totalSecondsInTimePeriod: number;
    } = UptimeUtil.getTotalDowntimeInSeconds(
      timeline,
      statusPage.downtimeMonitorStatuses!,
    );

    return {
      reportDates: startAndEndDate,
      totalResources: statusPageResources.length,
      totalIncidents: incidentCount,
      averageUptimePercent: avgUptimePercentString,
      resources: reportItems,
      totalDowntimeInHoursAndMinutes:
        OneUptimeDate.convertMinutesToDaysHoursAndMinutes(
          Math.ceil(totalDowntimeInSeconds.totalDowntimeInSeconds / 60),
        ),
    };
  }

  @CaptureSpan()
  public async getIncidentCountByMonitorIds(data: {
    monitorIds: Array<ObjectID>;
    historyDays: number;
  }): Promise<number> {
    const today: Date = OneUptimeDate.getCurrentDate();

    const historyDays: Date = OneUptimeDate.getSomeDaysAgo(
      data.historyDays || 14,
    );

    const incidentCount: PositiveNumber = await IncidentService.countBy({
      query: {
        monitors: data.monitorIds as any,
        createdAt: QueryHelper.inBetween(historyDays, today),
      },
      props: {
        isRoot: true,
      },
    });

    return incidentCount.toNumber();
  }

  @CaptureSpan()
  public async getIncidentCountOnStatusPage(data: {
    statusPageId: ObjectID;
    historyDays: number;
  }): Promise<number> {
    const monitorsOnStatusPage: {
      monitorsOnStatusPage: Array<ObjectID>;
      monitorsInGroup: Dictionary<Array<ObjectID>>;
    } = await this.getMonitorIdsOnStatusPage({
      statusPageId: data.statusPageId,
    });

    return this.getIncidentCountByMonitorIds({
      monitorIds: monitorsOnStatusPage.monitorsOnStatusPage,
      historyDays: data.historyDays,
    });
  }

  @CaptureSpan()
  public async getMonitorIdsOnStatusPage(data: {
    statusPageId: ObjectID;
  }): Promise<{
    monitorsOnStatusPage: Array<ObjectID>;
    monitorsInGroup: Dictionary<Array<ObjectID>>;
  }> {
    const statusPageResources: Array<StatusPageResource> =
      await this.getStatusPageResources(data);

    const monitorGroupIds: Array<ObjectID> = statusPageResources
      .map((resource: StatusPageResource) => {
        return resource.monitorGroupId!;
      })
      .filter((id: ObjectID) => {
        return Boolean(id); // remove nulls
      });

    const monitorsInGroup: Dictionary<Array<ObjectID>> = {};

    // get monitor status charts.
    const monitorsOnStatusPage: Array<ObjectID> = statusPageResources
      .map((monitor: StatusPageResource) => {
        return monitor.monitorId!;
      })
      .filter((id: ObjectID) => {
        return Boolean(id); // remove nulls
      });

    for (const monitorGroupId of monitorGroupIds) {
      // get current status of monitors in the group.

      // get monitors in the group.

      const groupResources: Array<MonitorGroupResource> =
        await MonitorGroupResourceService.findBy({
          query: {
            monitorGroupId: monitorGroupId,
          },
          select: {
            monitorId: true,
          },
          props: {
            isRoot: true,
          },
          limit: LIMIT_PER_PROJECT,
          skip: 0,
        });

      const monitorsInGroupIds: Array<ObjectID> = groupResources
        .map((resource: MonitorGroupResource) => {
          return resource.monitorId!;
        })
        .filter((id: ObjectID) => {
          return Boolean(id); // remove nulls
        });

      for (const monitorId of monitorsInGroupIds) {
        if (
          !monitorsOnStatusPage.find((item: ObjectID) => {
            return item.toString() === monitorId.toString();
          })
        ) {
          monitorsOnStatusPage.push(monitorId);
        }
      }

      monitorsInGroup[monitorGroupId.toString()] = monitorsInGroupIds;
    }

    return {
      monitorsOnStatusPage: monitorsOnStatusPage,
      monitorsInGroup: monitorsInGroup,
    };
  }

  @CaptureSpan()
  public async getStatusPageResources(data: {
    statusPageId: ObjectID;
  }): Promise<Array<StatusPageResource>> {
    // get monitors on status page.
    const statusPageResources: Array<StatusPageResource> =
      await StatusPageResourceService.findBy({
        query: {
          statusPageId: data.statusPageId,
        },
        select: {
          statusPageGroupId: true,
          monitorId: true,
          displayTooltip: true,
          displayDescription: true,
          displayName: true,
          monitor: {
            _id: true,
            currentMonitorStatusId: true,
          },
          monitorGroupId: true,
          order: true,
        },
        skip: 0,
        limit: LIMIT_PER_PROJECT,
        props: {
          isRoot: true,
        },
      });

    // sort by order and then return

    return statusPageResources.sort(
      (a: StatusPageResource, b: StatusPageResource) => {
        return a.order! - b.order!;
      },
    );
  }
}
export default new Service();
