import ObjectID from "../../Types/ObjectID";
import AlertGroupingRule from "../../Models/DatabaseModels/AlertGroupingRule";
import Alert from "../../Models/DatabaseModels/Alert";
import AlertEpisode from "../../Models/DatabaseModels/AlertEpisode";
import AlertEpisodeMember, {
  AlertEpisodeMemberAddedBy,
} from "../../Models/DatabaseModels/AlertEpisodeMember";
import AlertEpisodeOwnerUser from "../../Models/DatabaseModels/AlertEpisodeOwnerUser";
import AlertEpisodeOwnerTeam from "../../Models/DatabaseModels/AlertEpisodeOwnerTeam";
import Label from "../../Models/DatabaseModels/Label";
import Monitor from "../../Models/DatabaseModels/Monitor";
import AlertSeverity from "../../Models/DatabaseModels/AlertSeverity";
import ServiceMonitor from "../../Models/DatabaseModels/ServiceMonitor";
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
import logger from "../Utils/Logger";
import SortOrder from "../../Types/BaseDatabase/SortOrder";
import OneUptimeDate from "../../Types/Date";
import QueryHelper from "../Types/Database/QueryHelper";
import AlertGroupingRuleService from "./AlertGroupingRuleService";
import AlertEpisodeService from "./AlertEpisodeService";
import AlertEpisodeMemberService from "./AlertEpisodeMemberService";
import AlertEpisodeOwnerUserService from "./AlertEpisodeOwnerUserService";
import AlertEpisodeOwnerTeamService from "./AlertEpisodeOwnerTeamService";
import MonitorService from "./MonitorService";
import ServiceMonitorService from "./ServiceMonitorService";
import Semaphore, { SemaphoreMutex } from "../Infrastructure/Semaphore";
import AlertEpisodeFeedService from "./AlertEpisodeFeedService";
import { AlertEpisodeFeedEventType } from "../../Models/DatabaseModels/AlertEpisodeFeed";
import { Green500 } from "../../Types/BrandColors";

export interface GroupingResult {
  grouped: boolean;
  episodeId?: ObjectID;
  isNewEpisode?: boolean;
  wasReopened?: boolean;
}

class AlertGroupingEngineServiceClass {
  @CaptureSpan()
  public async processAlert(alert: Alert): Promise<GroupingResult> {
    logger.debug(`Processing alert ${alert.id} for grouping`);

    try {
      if (!alert.id || !alert.projectId) {
        logger.warn("Alert missing id or projectId, skipping grouping");
        return { grouped: false };
      }

      // If alert already has an episode, don't reprocess
      if (alert.alertEpisodeId) {
        return { grouped: true, episodeId: alert.alertEpisodeId };
      }

      // Get enabled rules sorted by priority
      const rules: Array<AlertGroupingRule> =
        await AlertGroupingRuleService.findBy({
          query: {
            projectId: alert.projectId,
            isEnabled: true,
          },
          sort: {
            priority: SortOrder.Ascending,
          },
          props: {
            isRoot: true,
          },
          select: {
            _id: true,
            name: true,
            priority: true,
            // Match criteria fields
            monitors: {
              _id: true,
            },
            alertSeverities: {
              _id: true,
            },
            alertLabels: {
              _id: true,
            },
            monitorLabels: {
              _id: true,
            },
            alertTitlePattern: true,
            alertDescriptionPattern: true,
            monitorNamePattern: true,
            monitorDescriptionPattern: true,
            // Group by fields
            groupByMonitor: true,
            groupBySeverity: true,
            groupByAlertTitle: true,
            groupByService: true,
            // Time settings
            enableTimeWindow: true,
            timeWindowMinutes: true,
            episodeTitleTemplate: true,
            episodeDescriptionTemplate: true,
            enableResolveDelay: true,
            resolveDelayMinutes: true,
            enableReopenWindow: true,
            reopenWindowMinutes: true,
            enableInactivityTimeout: true,
            inactivityTimeoutMinutes: true,
            defaultAssignToUserId: true,
            defaultAssignToTeamId: true,
            onCallDutyPolicies: {
              _id: true,
            },
            // Episode configuration fields
            episodeLabels: {
              _id: true,
            },
            episodeOwnerUsers: {
              _id: true,
            },
            episodeOwnerTeams: {
              _id: true,
            },
          },
          limit: 100,
          skip: 0,
        });

      if (rules.length === 0) {
        logger.debug(
          `No enabled grouping rules found for project ${alert.projectId}`,
        );
        return { grouped: false };
      }

      logger.debug(
        `Found ${rules.length} enabled grouping rules for project ${alert.projectId}`,
      );

      // Find first matching rule
      for (const rule of rules) {
        const matches: boolean = await this.doesAlertMatchRule(alert, rule);

        if (matches) {
          logger.debug(
            `Alert ${alert.id} matches rule ${rule.name || rule.id}`,
          );

          // Try to find existing episode or create new one
          const result: GroupingResult = await this.groupAlertWithRule(
            alert,
            rule,
          );
          return result;
        }
      }

      logger.debug(`Alert ${alert.id} did not match any grouping rules`);
      return { grouped: false };
    } catch (error) {
      logger.error(`Error processing alert for grouping: ${error}`);
      return { grouped: false };
    }
  }

  @CaptureSpan()
  private async doesAlertMatchRule(
    alert: Alert,
    rule: AlertGroupingRule,
  ): Promise<boolean> {
    logger.debug(
      `Checking if alert ${alert.id} matches rule ${rule.name || rule.id}`,
    );

    // Check monitor IDs - if monitors are specified, alert must be from one of them
    if (rule.monitors && rule.monitors.length > 0) {
      if (!alert.monitorId) {
        return false;
      }
      const monitorIds: Array<string> = rule.monitors.map((m: Monitor) => {
        return m.id?.toString() || "";
      });
      const alertMonitorIdStr: string = alert.monitorId.toString();
      if (!monitorIds.includes(alertMonitorIdStr)) {
        return false;
      }
    }

    // Check alert severity IDs - if severities are specified, alert must have one of them
    if (rule.alertSeverities && rule.alertSeverities.length > 0) {
      if (!alert.alertSeverityId) {
        return false;
      }
      const severityIds: Array<string> = rule.alertSeverities.map(
        (s: AlertSeverity) => {
          return s.id?.toString() || "";
        },
      );
      const alertSeverityIdStr: string = alert.alertSeverityId.toString();
      if (!severityIds.includes(alertSeverityIdStr)) {
        return false;
      }
    }

    // Check alert label IDs - if alert labels are specified, alert must have at least one of them
    if (rule.alertLabels && rule.alertLabels.length > 0) {
      if (!alert.labels || alert.labels.length === 0) {
        return false;
      }
      const ruleLabelIds: Array<string> = rule.alertLabels.map((l: Label) => {
        return l.id?.toString() || "";
      });
      const alertLabelIds: Array<string> = alert.labels.map((l: Label) => {
        return l.id?.toString() || "";
      });
      const hasMatchingLabel: boolean = ruleLabelIds.some((labelId: string) => {
        return alertLabelIds.includes(labelId);
      });
      if (!hasMatchingLabel) {
        return false;
      }
    }

    // Check monitor-related criteria (labels, name pattern, description pattern)
    const hasMonitorCriteria: boolean = Boolean(
      (rule.monitorLabels && rule.monitorLabels.length > 0) ||
        rule.monitorNamePattern ||
        rule.monitorDescriptionPattern,
    );

    if (hasMonitorCriteria) {
      if (!alert.monitorId) {
        return false;
      }

      // Load monitor with all needed fields
      const monitor: Monitor | null = await MonitorService.findOneById({
        id: alert.monitorId,
        select: {
          name: true,
          description: true,
          labels: {
            _id: true,
          },
        },
        props: {
          isRoot: true,
        },
      });

      if (!monitor) {
        return false;
      }

      // Check monitor labels
      if (rule.monitorLabels && rule.monitorLabels.length > 0) {
        if (!monitor.labels || monitor.labels.length === 0) {
          return false;
        }

        const ruleMonitorLabelIds: Array<string> = rule.monitorLabels.map(
          (l: Label) => {
            return l.id?.toString() || "";
          },
        );
        const monitorLabelIds: Array<string> = monitor.labels.map(
          (l: Label) => {
            return l.id?.toString() || "";
          },
        );
        const hasMatchingMonitorLabel: boolean = ruleMonitorLabelIds.some(
          (labelId: string) => {
            return monitorLabelIds.includes(labelId);
          },
        );
        if (!hasMatchingMonitorLabel) {
          return false;
        }
      }

      // Check monitor name pattern (regex)
      if (rule.monitorNamePattern) {
        if (!monitor.name) {
          return false;
        }
        try {
          const regex: RegExp = new RegExp(rule.monitorNamePattern, "i");
          if (!regex.test(monitor.name)) {
            return false;
          }
        } catch {
          logger.warn(
            `Invalid regex pattern in rule ${rule.id}: ${rule.monitorNamePattern}`,
          );
          return false;
        }
      }

      // Check monitor description pattern (regex)
      if (rule.monitorDescriptionPattern) {
        if (!monitor.description) {
          return false;
        }
        try {
          const regex: RegExp = new RegExp(rule.monitorDescriptionPattern, "i");
          if (!regex.test(monitor.description)) {
            return false;
          }
        } catch {
          logger.warn(
            `Invalid regex pattern in rule ${rule.id}: ${rule.monitorDescriptionPattern}`,
          );
          return false;
        }
      }
    }

    // Check alert title pattern (regex)
    if (rule.alertTitlePattern) {
      if (!alert.title) {
        return false;
      }
      try {
        const regex: RegExp = new RegExp(rule.alertTitlePattern, "i");
        if (!regex.test(alert.title)) {
          return false;
        }
      } catch {
        logger.warn(
          `Invalid regex pattern in rule ${rule.id}: ${rule.alertTitlePattern}`,
        );
        return false;
      }
    }

    // Check alert description pattern (regex)
    if (rule.alertDescriptionPattern) {
      if (!alert.description) {
        return false;
      }
      try {
        const regex: RegExp = new RegExp(rule.alertDescriptionPattern, "i");
        if (!regex.test(alert.description)) {
          return false;
        }
      } catch {
        logger.warn(
          `Invalid regex pattern in rule ${rule.id}: ${rule.alertDescriptionPattern}`,
        );
        return false;
      }
    }

    // If no criteria specified (all fields empty), rule matches all alerts
    logger.debug(
      `Rule ${rule.name || rule.id} matched alert ${alert.id} (all criteria passed)`,
    );
    return true;
  }

  @CaptureSpan()
  private async groupAlertWithRule(
    alert: Alert,
    rule: AlertGroupingRule,
  ): Promise<GroupingResult> {
    // Build the grouping key based on groupBy fields
    const groupingKey: string = await this.buildGroupingKey(alert, rule);

    // Create mutex key to prevent race conditions when creating episodes
    const mutexKey: string = `${alert.projectId?.toString()}-${rule.id?.toString()}-${groupingKey}`;

    let mutex: SemaphoreMutex | null = null;

    try {
      /*
       * Acquire mutex to prevent concurrent episode creation for the same grouping key
       * This is critical - we must have the lock before proceeding to prevent race conditions
       */
      logger.debug(
        `Acquiring mutex for grouping key: ${mutexKey} for alert ${alert.id}`,
      );
      mutex = await Semaphore.lock({
        key: mutexKey,
        namespace: "AlertGroupingEngine.groupAlertWithRule",
        lockTimeout: 30000, // 30 seconds - enough time to complete episode creation
        acquireTimeout: 60000, // Wait up to 60 seconds to acquire the lock
      });
      logger.debug(
        `Acquired mutex for grouping key: ${mutexKey} for alert ${alert.id}`,
      );

      // Calculate time window cutoff (only if time window is enabled)
      let timeWindowCutoff: Date | null = null;
      if (rule.enableTimeWindow) {
        const timeWindowMinutes: number = rule.timeWindowMinutes || 60;
        timeWindowCutoff = OneUptimeDate.getSomeMinutesAgo(timeWindowMinutes);
      }

      // Find existing active episode that matches
      const existingEpisode: AlertEpisode | null =
        await this.findMatchingActiveEpisode(
          alert.projectId!,
          rule.id!,
          groupingKey,
          timeWindowCutoff,
        );

      if (existingEpisode && existingEpisode.id) {
        // Add alert to existing episode
        await this.addAlertToEpisode(
          alert,
          existingEpisode.id,
          AlertEpisodeMemberAddedBy.Rule,
          rule.id!,
        );

        // Update episode severity if alert has higher severity
        if (alert.alertSeverityId) {
          await AlertEpisodeService.updateEpisodeSeverity({
            episodeId: existingEpisode.id,
            severityId: alert.alertSeverityId,
            onlyIfHigher: true,
          });
        }

        return {
          grouped: true,
          episodeId: existingEpisode.id,
          isNewEpisode: false,
        };
      }

      // Check if we can reopen a recently resolved episode (only if enabled)
      if (rule.enableReopenWindow) {
        const reopenWindowMinutes: number = rule.reopenWindowMinutes || 0;
        if (reopenWindowMinutes > 0) {
          const reopenCutoff: Date =
            OneUptimeDate.getSomeMinutesAgo(reopenWindowMinutes);
          const recentlyResolvedEpisode: AlertEpisode | null =
            await this.findRecentlyResolvedEpisode(
              alert.projectId!,
              rule.id!,
              groupingKey,
              reopenCutoff,
            );

          if (recentlyResolvedEpisode && recentlyResolvedEpisode.id) {
            // Reopen the episode
            await AlertEpisodeService.reopenEpisode(recentlyResolvedEpisode.id);

            // Add alert to reopened episode
            await this.addAlertToEpisode(
              alert,
              recentlyResolvedEpisode.id,
              AlertEpisodeMemberAddedBy.Rule,
              rule.id!,
            );

            // Update episode severity if alert has higher severity
            if (alert.alertSeverityId) {
              await AlertEpisodeService.updateEpisodeSeverity({
                episodeId: recentlyResolvedEpisode.id,
                severityId: alert.alertSeverityId,
                onlyIfHigher: true,
              });
            }

            return {
              grouped: true,
              episodeId: recentlyResolvedEpisode.id,
              isNewEpisode: false,
              wasReopened: true,
            };
          }
        }
      }

      // Create new episode
      const newEpisode: AlertEpisode | null = await this.createNewEpisode(
        alert,
        rule,
        groupingKey,
      );

      if (newEpisode && newEpisode.id) {
        // Add alert to new episode
        await this.addAlertToEpisode(
          alert,
          newEpisode.id,
          AlertEpisodeMemberAddedBy.Rule,
          rule.id!,
        );

        return { grouped: true, episodeId: newEpisode.id, isNewEpisode: true };
      }

      return { grouped: false };
    } finally {
      // Release mutex
      if (mutex) {
        try {
          logger.debug(
            `Releasing mutex for grouping key: ${mutexKey} for alert ${alert.id}`,
          );
          await Semaphore.release(mutex);
          logger.debug(
            `Released mutex for grouping key: ${mutexKey} for alert ${alert.id}`,
          );
        } catch (err) {
          logger.error(
            `Error releasing mutex for grouping key: ${mutexKey}: ${err}`,
          );
        }
      }
    }
  }

  @CaptureSpan()
  private async buildGroupingKey(
    alert: Alert,
    rule: AlertGroupingRule,
  ): Promise<string> {
    const parts: Array<string> = [];

    /*
     * Group by service - only if explicitly enabled
     * Must be checked before monitor since service contains multiple monitors
     */
    if (rule.groupByService && alert.monitorId) {
      const serviceMonitor: ServiceMonitor | null =
        await ServiceMonitorService.findOneBy({
          query: {
            monitorId: alert.monitorId,
          },
          select: {
            serviceId: true,
          },
          props: {
            isRoot: true,
          },
        });

      if (serviceMonitor?.serviceId) {
        parts.push(`service:${serviceMonitor.serviceId.toString()}`);
      }
    }

    // Group by monitor - only if explicitly enabled
    if (rule.groupByMonitor && alert.monitorId) {
      parts.push(`monitor:${alert.monitorId.toString()}`);
    }

    // Group by severity - only if explicitly enabled
    if (rule.groupBySeverity && alert.alertSeverityId) {
      parts.push(`severity:${alert.alertSeverityId.toString()}`);
    }

    // Group by alert title - only if explicitly enabled
    if (rule.groupByAlertTitle && alert.title) {
      // Normalize title for grouping (remove numbers, etc.)
      const normalizedTitle: string = alert.title
        .toLowerCase()
        .replace(/\d+/g, "X");
      parts.push(`title:${normalizedTitle}`);
    }

    // If no group by options are enabled, all matching alerts go into a single episode
    return parts.join("|") || "default";
  }

  @CaptureSpan()
  private async findMatchingActiveEpisode(
    projectId: ObjectID,
    ruleId: ObjectID,
    groupingKey: string,
    timeWindowCutoff: Date | null,
  ): Promise<AlertEpisode | null> {
    /*
     * Find active episode with matching rule and grouping key
     * Active episodes have resolvedAt = null (not yet resolved)
     * If time window is enabled, also filter by lastAlertAddedAt
     * If time window is disabled (timeWindowCutoff is null), find any matching active episode
     */
    interface EpisodeQueryType {
      projectId: ObjectID;
      alertGroupingRuleId: ObjectID;
      groupingKey: string;
      resolvedAt: null;
      lastAlertAddedAt?: ReturnType<typeof QueryHelper.greaterThanEqualTo>;
    }

    const query: EpisodeQueryType = {
      projectId: projectId,
      alertGroupingRuleId: ruleId,
      groupingKey: groupingKey,
      resolvedAt: null, // Only find active (non-resolved) episodes
    };

    // Only add time window filter if enabled
    if (timeWindowCutoff) {
      query.lastAlertAddedAt = QueryHelper.greaterThanEqualTo(timeWindowCutoff);
    }

    const episode: AlertEpisode | null = await AlertEpisodeService.findOneBy({
      query: query as any,
      sort: {
        lastAlertAddedAt: SortOrder.Descending,
      },
      select: {
        _id: true,
        lastAlertAddedAt: true,
      },
      props: {
        isRoot: true,
      },
    });

    return episode;
  }

  @CaptureSpan()
  private async findRecentlyResolvedEpisode(
    projectId: ObjectID,
    ruleId: ObjectID,
    groupingKey: string,
    reopenCutoff: Date,
  ): Promise<AlertEpisode | null> {
    // Find recently resolved episode with matching rule and grouping key
    const episode: AlertEpisode | null = await AlertEpisodeService.findOneBy({
      query: {
        projectId: projectId,
        alertGroupingRuleId: ruleId,
        groupingKey: groupingKey,
        resolvedAt: QueryHelper.greaterThanEqualTo(reopenCutoff),
      },
      sort: {
        resolvedAt: SortOrder.Descending,
      },
      select: {
        _id: true,
        resolvedAt: true,
      },
      props: {
        isRoot: true,
      },
    });

    return episode;
  }

  @CaptureSpan()
  private async createNewEpisode(
    alert: Alert,
    rule: AlertGroupingRule,
    groupingKey: string,
  ): Promise<AlertEpisode | null> {
    // Generate episode title from template (with initial alertCount of 1)
    const title: string = this.generateEpisodeTitle(
      alert,
      rule.episodeTitleTemplate,
      1, // Initial alert count
    );

    // Generate episode description from template (with initial alertCount of 1)
    const description: string | undefined = this.generateEpisodeDescription(
      alert,
      rule.episodeDescriptionTemplate,
      1, // Initial alert count
    );

    const newEpisode: AlertEpisode = new AlertEpisode();
    newEpisode.projectId = alert.projectId!;
    newEpisode.title = title;
    if (description) {
      newEpisode.description = description;
    }
    /*
     * Store preprocessed templates for dynamic variable updates
     * Static variables are replaced, dynamic ones (like {{alertCount}}) remain as placeholders
     */
    if (rule.episodeTitleTemplate) {
      newEpisode.titleTemplate = this.preprocessTemplate(
        alert,
        rule.episodeTitleTemplate,
      );
    }
    if (rule.episodeDescriptionTemplate) {
      newEpisode.descriptionTemplate = this.preprocessTemplate(
        alert,
        rule.episodeDescriptionTemplate,
      );
    }
    newEpisode.alertGroupingRuleId = rule.id!;
    newEpisode.groupingKey = groupingKey;
    newEpisode.isManuallyCreated = false;
    newEpisode.lastAlertAddedAt = OneUptimeDate.getCurrentDate();

    // Set severity from alert
    if (alert.alertSeverityId) {
      newEpisode.alertSeverityId = alert.alertSeverityId;
    }

    // Set default ownership from rule
    if (rule.defaultAssignToUserId) {
      newEpisode.assignedToUserId = rule.defaultAssignToUserId;
    }

    if (rule.defaultAssignToTeamId) {
      newEpisode.assignedToTeamId = rule.defaultAssignToTeamId;
    }

    // Copy on-call policies from rule
    if (rule.onCallDutyPolicies && rule.onCallDutyPolicies.length > 0) {
      newEpisode.onCallDutyPolicies = rule.onCallDutyPolicies;
    }

    // Copy episode labels from rule
    if (rule.episodeLabels && rule.episodeLabels.length > 0) {
      newEpisode.labels = rule.episodeLabels;
    }

    try {
      const createdEpisode: AlertEpisode = await AlertEpisodeService.create({
        data: newEpisode,
        props: {
          isRoot: true,
        },
      });

      // Add episode owner users from rule
      if (
        rule.episodeOwnerUsers &&
        rule.episodeOwnerUsers.length > 0 &&
        createdEpisode.id
      ) {
        for (const user of rule.episodeOwnerUsers) {
          if (!user.id) {
            continue;
          }
          try {
            const ownerUser: AlertEpisodeOwnerUser =
              new AlertEpisodeOwnerUser();
            ownerUser.projectId = alert.projectId!;
            ownerUser.alertEpisodeId = createdEpisode.id;
            ownerUser.userId = user.id;
            await AlertEpisodeOwnerUserService.create({
              data: ownerUser,
              props: {
                isRoot: true,
              },
            });
          } catch (ownerError) {
            logger.error(
              `Error adding owner user ${user.id} to episode: ${ownerError}`,
            );
          }
        }
      }

      // Add episode owner teams from rule
      if (
        rule.episodeOwnerTeams &&
        rule.episodeOwnerTeams.length > 0 &&
        createdEpisode.id
      ) {
        for (const team of rule.episodeOwnerTeams) {
          if (!team.id) {
            continue;
          }
          try {
            const ownerTeam: AlertEpisodeOwnerTeam =
              new AlertEpisodeOwnerTeam();
            ownerTeam.projectId = alert.projectId!;
            ownerTeam.alertEpisodeId = createdEpisode.id;
            ownerTeam.teamId = team.id;
            await AlertEpisodeOwnerTeamService.create({
              data: ownerTeam,
              props: {
                isRoot: true,
              },
            });
          } catch (ownerError) {
            logger.error(
              `Error adding owner team ${team.id} to episode: ${ownerError}`,
            );
          }
        }
      }

      // Add episode feed entry for episode creation
      if (createdEpisode.id) {
        const groupByParts: Array<string> = [];

        if (rule.groupByMonitor) {
          groupByParts.push("Monitor");
        }
        if (rule.groupBySeverity) {
          groupByParts.push("Severity");
        }
        if (rule.groupByAlertTitle) {
          groupByParts.push("Alert Title");
        }
        if (rule.groupByService) {
          groupByParts.push("Service");
        }

        const groupByDescription: string =
          groupByParts.length > 0
            ? `Grouping by: ${groupByParts.join(", ")}`
            : "Grouping all matching alerts together";

        let moreInfo: string = `**Rule:** ${rule.name || "Unnamed Rule"}\n\n`;
        moreInfo += `**Grouping Key:** \`${groupingKey}\`\n\n`;
        moreInfo += `**${groupByDescription}**`;

        if (rule.enableTimeWindow && rule.timeWindowMinutes) {
          moreInfo += `\n\n**Time Window:** ${rule.timeWindowMinutes} minutes`;
        }

        try {
          await AlertEpisodeFeedService.createAlertEpisodeFeedItem({
            alertEpisodeId: createdEpisode.id,
            projectId: alert.projectId!,
            alertEpisodeFeedEventType: AlertEpisodeFeedEventType.EpisodeCreated,
            displayColor: Green500,
            feedInfoInMarkdown: `🔔 **Episode Created** by grouping rule **${rule.name || "Unnamed Rule"}**`,
            moreInformationInMarkdown: moreInfo,
          });
        } catch (feedError) {
          logger.error(
            `Error creating episode feed for episode creation: ${feedError}`,
          );
        }
      }

      return createdEpisode;
    } catch (error) {
      logger.error(`Error creating new episode: ${error}`);
      return null;
    }
  }

  private generateEpisodeTitle(
    alert: Alert,
    template: string | undefined,
    alertCount: number = 1,
  ): string {
    if (!template) {
      // Default title based on alert
      if (alert.monitor?.name) {
        return alert.monitor.name;
      }
      if (alert.title) {
        return alert.title.substring(0, 50);
      }
      return "Untitled Episode";
    }

    return (
      this.replaceTemplatePlaceholders(alert, template, alertCount) ||
      "Untitled Episode"
    );
  }

  private generateEpisodeDescription(
    alert: Alert,
    template: string | undefined,
    alertCount: number = 1,
  ): string | undefined {
    if (!template) {
      return undefined;
    }

    return (
      this.replaceTemplatePlaceholders(alert, template, alertCount) || undefined
    );
  }

  private replaceTemplatePlaceholders(
    alert: Alert,
    template: string,
    alertCount: number = 1,
  ): string {
    let result: string = template;

    /*
     * Static variables (from first alert)
     * {{alertTitle}}
     */
    if (alert.title) {
      result = result.replace(/\{\{alertTitle\}\}/g, alert.title);
    }

    // {{alertDescription}}
    if (alert.description) {
      result = result.replace(/\{\{alertDescription\}\}/g, alert.description);
    }

    // {{monitorName}}
    if (alert.monitor?.name) {
      result = result.replace(/\{\{monitorName\}\}/g, alert.monitor.name);
    }

    // {{alertSeverity}}
    if (alert.alertSeverity?.name) {
      result = result.replace(
        /\{\{alertSeverity\}\}/g,
        alert.alertSeverity.name,
      );
    }

    /*
     * Dynamic variables (updated when alerts are added/removed)
     * {{alertCount}}
     */
    result = result.replace(/\{\{alertCount\}\}/g, alertCount.toString());

    // Clean up any remaining unknown placeholders
    result = result.replace(/\{\{[^}]+\}\}/g, "");

    return result;
  }

  /*
   * Preprocess template: replace static variables but keep dynamic ones as placeholders
   * This is stored on the episode so we can re-render with updated dynamic values later
   */
  private preprocessTemplate(alert: Alert, template: string): string {
    let result: string = template;

    /*
     * Replace static variables (from first alert)
     * {{alertTitle}}
     */
    if (alert.title) {
      result = result.replace(/\{\{alertTitle\}\}/g, alert.title);
    }

    // {{alertDescription}}
    if (alert.description) {
      result = result.replace(/\{\{alertDescription\}\}/g, alert.description);
    }

    // {{monitorName}}
    if (alert.monitor?.name) {
      result = result.replace(/\{\{monitorName\}\}/g, alert.monitor.name);
    }

    // {{alertSeverity}}
    if (alert.alertSeverity?.name) {
      result = result.replace(
        /\{\{alertSeverity\}\}/g,
        alert.alertSeverity.name,
      );
    }

    /*
     * Keep dynamic variables as placeholders (e.g., {{alertCount}})
     * They will be replaced when title/description is re-rendered
     */

    return result;
  }

  @CaptureSpan()
  private async addAlertToEpisode(
    alert: Alert,
    episodeId: ObjectID,
    addedBy: AlertEpisodeMemberAddedBy,
    ruleId?: ObjectID,
  ): Promise<void> {
    const member: AlertEpisodeMember = new AlertEpisodeMember();
    member.projectId = alert.projectId!;
    member.alertEpisodeId = episodeId;
    member.alertId = alert.id!;
    member.addedBy = addedBy;

    if (ruleId) {
      member.matchedRuleId = ruleId;
    }

    try {
      await AlertEpisodeMemberService.create({
        data: member,
        props: {
          isRoot: true,
        },
      });

      // Feed entries are created by AlertEpisodeMemberService.onCreateSuccess
    } catch (error) {
      // Check if it's a duplicate error (alert already in episode)
      if (
        error instanceof Error &&
        error.message.includes("already a member")
      ) {
        logger.debug(`Alert ${alert.id} is already in episode ${episodeId}`);
        return;
      }
      throw error;
    }
  }

  @CaptureSpan()
  public async addAlertToEpisodeManually(
    alert: Alert,
    episodeId: ObjectID,
    addedByUserId?: ObjectID,
  ): Promise<void> {
    const member: AlertEpisodeMember = new AlertEpisodeMember();
    member.projectId = alert.projectId!;
    member.alertEpisodeId = episodeId;
    member.alertId = alert.id!;
    member.addedBy = AlertEpisodeMemberAddedBy.Manual;

    if (addedByUserId) {
      member.addedByUserId = addedByUserId;
    }

    await AlertEpisodeMemberService.create({
      data: member,
      props: {
        isRoot: true,
      },
    });

    // Feed entries are created by AlertEpisodeMemberService.onCreateSuccess

    // Update episode severity if needed
    if (alert.alertSeverityId) {
      await AlertEpisodeService.updateEpisodeSeverity({
        episodeId: episodeId,
        severityId: alert.alertSeverityId,
        onlyIfHigher: true,
      });
    }
  }
}

export default new AlertGroupingEngineServiceClass();
