import ObjectID from "../../Types/ObjectID";
import DatabaseService from "./DatabaseService";
import OneUptimeDate from "../../Types/Date";
import BadDataException from "../../Types/Exception/BadDataException";
import MonitorProbe from "../../Models/DatabaseModels/MonitorProbe";
import QueryHelper from "../Types/Database/QueryHelper";
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
import MonitorService from "./MonitorService";
import { MonitorTypeHelper } from "../../Types/Monitor/MonitorType";
import CronTab from "../Utils/CronTab";
import logger from "../Utils/Logger";
export class Service extends DatabaseService {
    constructor() {
        super(MonitorProbe);
    }
    async pruneStaleLastMonitoringLogEntries(data) {
        const validIdSet = new Set(data.validMonitorStepIds);
        const monitorProbes = await this.findBy({
            query: {
                monitorId: data.monitorId,
            },
            select: {
                _id: true,
                lastMonitoringLog: true,
            },
            limit: LIMIT_PER_PROJECT,
            skip: 0,
            props: {
                isRoot: true,
            },
        });
        for (const monitorProbe of monitorProbes) {
            if (!monitorProbe.id || !monitorProbe.lastMonitoringLog) {
                continue;
            }
            const existingLog = monitorProbe.lastMonitoringLog;
            const existingKeys = Object.keys(existingLog);
            const prunedLog = {};
            let removedAny = false;
            for (const key of existingKeys) {
                if (validIdSet.has(key)) {
                    prunedLog[key] = existingLog[key];
                }
                else {
                    removedAny = true;
                }
            }
            if (!removedAny) {
                continue;
            }
            await this.updateOneById({
                id: monitorProbe.id,
                data: {
                    lastMonitoringLog: prunedLog,
                },
                props: {
                    isRoot: true,
                },
            });
        }
    }
    async updateNextPingAtForMonitor(data) {
        var _a, _b;
        const monitorProbes = await this.findBy({
            query: {
                monitorId: data.monitorId,
            },
            select: {
                nextPingAt: true,
                probeId: true,
                monitor: {
                    monitoringInterval: true,
                },
            },
            limit: LIMIT_PER_PROJECT,
            skip: 0,
            props: {
                isRoot: true,
            },
        });
        for (const monitorProbe of monitorProbes) {
            if (!monitorProbe.probeId) {
                continue;
            }
            let nextPing = OneUptimeDate.addRemoveMinutes(OneUptimeDate.getCurrentDate(), 1);
            try {
                nextPing = CronTab.getNextExecutionTime((_a = monitorProbe === null || monitorProbe === void 0 ? void 0 : monitorProbe.monitor) === null || _a === void 0 ? void 0 : _a.monitoringInterval);
            }
            catch (err) {
                logger.error(err, {
                    monitorId: (_b = data.monitorId) === null || _b === void 0 ? void 0 : _b.toString(),
                });
            }
            if (nextPing && monitorProbe.id) {
                await this.updateOneById({
                    id: monitorProbe.id,
                    data: {
                        nextPingAt: nextPing,
                    },
                    props: {
                        isRoot: true,
                    },
                });
            }
        }
    }
    /**
     * Atomically claims monitor probes for a specific probe instance.
     * Uses PostgreSQL's FOR UPDATE SKIP LOCKED to prevent multiple probe instances
     * from picking up the same monitors simultaneously.
     *
     * @param data - Object containing probeId, limit, and nextPingAt
     * @returns Array of claimed MonitorProbe IDs
     */
    async claimMonitorProbesForProbing(data) {
        const currentDate = OneUptimeDate.getCurrentDate();
        /*
         * Use a transaction with FOR UPDATE SKIP LOCKED to atomically claim monitors
         * This prevents multiple probe instances from picking up the same monitors
         */
        const claimedIds = await this.executeTransaction(async (transactionalEntityManager) => {
            /*
             * Select and lock the monitor probes that need to be processed,
             * including the monitoringInterval so we can compute the real nextPingAt
             * in a single UPDATE (avoiding a second round-trip).
             * FOR UPDATE SKIP LOCKED ensures that:
             * 1. Rows are locked for this transaction
             * 2. Rows already locked by other transactions are skipped
             */
            const selectQuery = `
        SELECT mp."_id", m."monitoringInterval"
        FROM "MonitorProbe" mp
        INNER JOIN "Monitor" m ON mp."monitorId" = m."_id"
        INNER JOIN "Project" p ON mp."projectId" = p."_id"
        WHERE mp."probeId" = $1
          AND mp."isEnabled" = true
          AND mp."deletedAt" IS NULL
          AND (mp."nextPingAt" IS NULL OR mp."nextPingAt" <= $2)
          AND m."disableActiveMonitoring" = false
          AND m."disableActiveMonitoringBecauseOfManualIncident" = false
          AND m."disableActiveMonitoringBecauseOfScheduledMaintenanceEvent" = false
          AND m."deletedAt" IS NULL
          AND p."deletedAt" IS NULL
          AND (p."paymentProviderSubscriptionStatus" IS NULL
               OR p."paymentProviderSubscriptionStatus" IN ('active', 'trialing'))
          AND (p."paymentProviderMeteredSubscriptionStatus" IS NULL
               OR p."paymentProviderMeteredSubscriptionStatus" IN ('active', 'trialing'))
        ORDER BY mp."nextPingAt" ASC NULLS FIRST
        LIMIT $3
        FOR UPDATE OF mp SKIP LOCKED
      `;
            const selectedRows = await transactionalEntityManager.query(selectQuery, [
                data.probeId.toString(),
                currentDate,
                data.limit,
            ]);
            if (selectedRows.length === 0) {
                return [];
            }
            // Compute the real nextPingAt per monitor and batch-update in one query
            const defaultNextPing = OneUptimeDate.addRemoveMinutes(currentDate, 1);
            const ids = [];
            const nextPingDates = [];
            const caseFragments = [];
            for (let i = 0; i < selectedRows.length; i++) {
                const row = selectedRows[i];
                ids.push(row._id);
                let nextPing = defaultNextPing;
                if (row.monitoringInterval) {
                    try {
                        nextPing = CronTab.getNextExecutionTime(row.monitoringInterval);
                    }
                    catch (_a) {
                        // fall back to default 1 minute
                    }
                }
                nextPingDates.push(nextPing);
                caseFragments.push(`WHEN '${row._id}'::uuid THEN $${i + 3}::timestamptz`);
            }
            const updateQuery = `
        UPDATE "MonitorProbe"
        SET "lastPingAt" = $1,
            "nextPingAt" = CASE "_id" ${caseFragments.join(" ")} END
        WHERE "_id" = ANY($2::uuid[])
      `;
            await transactionalEntityManager.query(updateQuery, [
                currentDate,
                ids,
                ...nextPingDates,
            ]);
            return ids.map((id) => {
                return new ObjectID(id);
            });
        });
        return claimedIds;
    }
    async onBeforeCreate(createBy) {
        var _a, _b, _c;
        if ((createBy.data.monitorId || createBy.data.monitor) &&
            (createBy.data.probeId || createBy.data.probe)) {
            const monitorProbe = await this.findOneBy({
                query: {
                    monitorId: createBy.data.monitorId || ((_a = createBy.data.monitor) === null || _a === void 0 ? void 0 : _a.id),
                    probeId: createBy.data.probeId || ((_b = createBy.data.probe) === null || _b === void 0 ? void 0 : _b.id),
                },
                select: {
                    _id: true,
                },
                props: {
                    isRoot: true,
                },
            });
            if (monitorProbe) {
                throw new BadDataException("Probe is already added to this monitor.");
            }
        }
        // Check if the monitor type supports probes
        const monitorId = createBy.data.monitorId || ((_c = createBy.data.monitor) === null || _c === void 0 ? void 0 : _c.id);
        if (monitorId) {
            const monitor = await MonitorService.findOneById({
                id: monitorId,
                select: {
                    monitorType: true,
                },
                props: {
                    isRoot: true,
                },
            });
            if ((monitor === null || monitor === void 0 ? void 0 : monitor.monitorType) &&
                !MonitorTypeHelper.isProbableMonitor(monitor.monitorType)) {
                throw new BadDataException("Probes cannot be added to this monitor type.");
            }
        }
        if (!createBy.data.nextPingAt) {
            createBy.data.nextPingAt = OneUptimeDate.getCurrentDate();
        }
        if (!createBy.data.lastPingAt) {
            createBy.data.lastPingAt = OneUptimeDate.getCurrentDate();
        }
        return { createBy, carryForward: null };
    }
    async onCreateSuccess(_onCreate, createdItem) {
        if (createdItem.probeId) {
            await MonitorService.refreshProbeStatus(createdItem.probeId);
        }
        return Promise.resolve(createdItem);
    }
    async onUpdateSuccess(onUpdate, updatedItemIds) {
        // if isEnabled is updated, refresh the probe status
        if (onUpdate.updateBy.data.isEnabled !== undefined) {
            const monitorProbes = await this.findBy({
                query: {
                    _id: QueryHelper.any(updatedItemIds),
                },
                select: {
                    monitorId: true,
                    probeId: true,
                    nextPingAt: true,
                },
                limit: LIMIT_PER_PROJECT,
                skip: 0,
                props: {
                    isRoot: true,
                },
            });
            for (const monitorProbe of monitorProbes) {
                if (!monitorProbe.probeId) {
                    continue;
                }
                await MonitorService.refreshProbeStatus(monitorProbe.probeId);
            }
        }
        return onUpdate;
    }
}
export default new Service();
//# sourceMappingURL=MonitorProbeService.js.map