var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
import DatabaseService from "./DatabaseService";
import KubernetesClusterLabelRuleEngineService from "./KubernetesClusterLabelRuleEngineService";
import KubernetesClusterOwnerRuleEngineService from "./KubernetesClusterOwnerRuleEngineService";
import Model from "../../Models/DatabaseModels/KubernetesCluster";
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
import ObjectID from "../../Types/ObjectID";
import QueryHelper from "../Types/Database/QueryHelper";
import OneUptimeDate from "../../Types/Date";
import LIMIT_MAX from "../../Types/Database/LimitMax";
import GlobalCache from "../Infrastructure/GlobalCache";
import logger from "../Utils/Logger";
import crypto from "crypto";
const LAST_SEEN_CACHE_NAMESPACE = "k8s-cluster-last-seen";
const LAST_SEEN_THROTTLE_SECONDS = 60;
const LABELS_APPLIED_CACHE_NAMESPACE = "k8s-cluster-labels-applied";
const LABELS_APPLIED_CACHE_TTL_SECONDS = 60;
export class Service extends DatabaseService {
    constructor() {
        super(Model);
    }
    async onCreateSuccess(_onCreate, createdItem) {
        if (createdItem.projectId && createdItem.id) {
            Promise.resolve()
                .then(async () => {
                await KubernetesClusterLabelRuleEngineService.applyRulesToKubernetesCluster(createdItem);
            })
                .then(async () => {
                await KubernetesClusterOwnerRuleEngineService.applyRulesToKubernetesCluster(createdItem);
            })
                .catch((error) => {
                var _a, _b;
                logger.error(`Error applying kubernetes cluster rules in KubernetesClusterService.onCreateSuccess: ${error}`, {
                    projectId: (_a = createdItem.projectId) === null || _a === void 0 ? void 0 : _a.toString(),
                    kubernetesClusterId: (_b = createdItem.id) === null || _b === void 0 ? void 0 : _b.toString(),
                });
            });
        }
        return createdItem;
    }
    async findOrCreateByClusterIdentifier(data) {
        /*
         * Look up case-insensitively. The unique guard on name/clusterIdentifier
         * (checkUniqueColumnBy -> findWithSameText) compares case-insensitively,
         * so a case-sensitive lookup would miss an existing row on casing drift
         * (k8s.cluster.name), then fail to create it ("KubernetesCluster with the
         * same name already exists") and wedge ingest. Mirrors
         * LabelService.findOrCreateLabelByName. Unlike HostService we keep the
         * stored casing as-is: k8s.cluster.name is not normalized at ingest, so
         * lowering the identifier here would desync it from the raw-cased
         * resource.k8s.cluster.name attribute the detail page filters on.
         */
        const existingCluster = await this.findOneBy({
            query: {
                projectId: data.projectId,
                clusterIdentifier: QueryHelper.findWithSameText(data.clusterIdentifier),
            },
            select: {
                _id: true,
                projectId: true,
                clusterIdentifier: true,
            },
            props: {
                isRoot: true,
            },
        });
        if (existingCluster) {
            return existingCluster;
        }
        try {
            // Create new cluster
            const newCluster = new Model();
            newCluster.projectId = data.projectId;
            newCluster.name = data.clusterIdentifier;
            newCluster.clusterIdentifier = data.clusterIdentifier;
            newCluster.otelCollectorStatus = "connected";
            newCluster.lastSeenAt = OneUptimeDate.getCurrentDate();
            const createdCluster = await this.create({
                data: newCluster,
                props: {
                    isRoot: true,
                },
            });
            return createdCluster;
        }
        catch (_a) {
            /*
             * Race condition: another request created the cluster concurrently.
             * Re-fetch the existing cluster.
             */
            const reFetchedCluster = await this.findOneBy({
                query: {
                    projectId: data.projectId,
                    clusterIdentifier: QueryHelper.findWithSameText(data.clusterIdentifier),
                },
                select: {
                    _id: true,
                    projectId: true,
                    clusterIdentifier: true,
                },
                props: {
                    isRoot: true,
                },
            });
            if (reFetchedCluster) {
                return reFetchedCluster;
            }
            throw new Error("Failed to create or find cluster: " + data.clusterIdentifier);
        }
    }
    async updateLastSeen(clusterId, extra) {
        var _a;
        const cacheKey = clusterId.toString();
        const extrasFingerprint = crypto
            .createHash("sha1")
            .update(JSON.stringify({
            agentVersion: (_a = extra === null || extra === void 0 ? void 0 : extra.agentVersion) !== null && _a !== void 0 ? _a : null,
        }))
            .digest("hex");
        const cached = await GlobalCache.getString(LAST_SEEN_CACHE_NAMESPACE, cacheKey);
        if (cached === extrasFingerprint) {
            return; // same data was written recently
        }
        await GlobalCache.setString(LAST_SEEN_CACHE_NAMESPACE, cacheKey, extrasFingerprint, { expiresInSeconds: LAST_SEEN_THROTTLE_SECONDS });
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const data = {
            lastSeenAt: OneUptimeDate.getCurrentDate(),
            otelCollectorStatus: "connected",
        };
        if (extra === null || extra === void 0 ? void 0 : extra.agentVersion) {
            data.agentVersion = extra.agentVersion;
        }
        await this.updateOneById({
            id: clusterId,
            data: data,
            props: {
                isRoot: true,
            },
        });
    }
    /**
     * Additively attach labels to a Kubernetes cluster. Existing labels
     * are never removed — manual labels set via the UI survive ingest.
     * The set of labelIds passed in is fingerprinted and cached for 60s
     * so the common case (steady-state collector pushing the same label
     * set every batch) costs one in-memory lookup, not a join-table
     * scan.
     */
    async attachLabels(data) {
        var _a;
        if (!data.labelIds || data.labelIds.length === 0) {
            return;
        }
        const cacheKey = data.kubernetesClusterId.toString();
        const fingerprint = fingerprintLabelIds(data.labelIds);
        const cached = await GlobalCache.getString(LABELS_APPLIED_CACHE_NAMESPACE, cacheKey);
        if (cached === fingerprint) {
            return;
        }
        try {
            const clusterIdStr = data.kubernetesClusterId.toString();
            const existingLabels = await this.getRepository()
                .createQueryBuilder()
                .relation(Model, "labels")
                .of(clusterIdStr)
                .loadMany();
            const existingIds = new Set();
            for (const lbl of existingLabels) {
                const idStr = (_a = lbl._id) === null || _a === void 0 ? void 0 : _a.toString();
                if (idStr) {
                    existingIds.add(idStr);
                }
            }
            const toAddIds = [];
            const seen = new Set();
            for (const id of data.labelIds) {
                const idStr = id.toString();
                if (existingIds.has(idStr) || seen.has(idStr)) {
                    continue;
                }
                seen.add(idStr);
                toAddIds.push(idStr);
            }
            if (toAddIds.length > 0) {
                await this.getRepository()
                    .createQueryBuilder()
                    .relation(Model, "labels")
                    .of(clusterIdStr)
                    .add(toAddIds);
            }
            await GlobalCache.setString(LABELS_APPLIED_CACHE_NAMESPACE, cacheKey, fingerprint, { expiresInSeconds: LABELS_APPLIED_CACHE_TTL_SECONDS });
        }
        catch (err) {
            logger.warn(`KubernetesClusterService.attachLabels failed for cluster ${data.kubernetesClusterId.toString()}: ${err instanceof Error ? err.message : String(err)}`);
        }
    }
    async markDisconnectedClusters() {
        const fiveMinutesAgo = OneUptimeDate.addRemoveMinutes(OneUptimeDate.getCurrentDate(), -5);
        const connectedClusters = await this.findBy({
            query: {
                otelCollectorStatus: "connected",
                lastSeenAt: QueryHelper.lessThan(fiveMinutesAgo),
            },
            select: {
                _id: true,
            },
            limit: LIMIT_MAX,
            skip: 0,
            props: {
                isRoot: true,
            },
        });
        for (const cluster of connectedClusters) {
            if (cluster._id) {
                await this.updateOneById({
                    id: new ObjectID(cluster._id.toString()),
                    data: {
                        otelCollectorStatus: "disconnected",
                    },
                    props: {
                        isRoot: true,
                    },
                });
            }
        }
    }
}
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object, Model]),
    __metadata("design:returntype", Promise)
], Service.prototype, "onCreateSuccess", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], Service.prototype, "findOrCreateByClusterIdentifier", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [ObjectID, Object]),
    __metadata("design:returntype", Promise)
], Service.prototype, "updateLastSeen", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], Service.prototype, "attachLabels", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", []),
    __metadata("design:returntype", Promise)
], Service.prototype, "markDisconnectedClusters", null);
function fingerprintLabelIds(labelIds) {
    const sorted = labelIds
        .map((id) => {
        return id.toString();
    })
        .sort();
    return crypto.createHash("sha1").update(sorted.join(",")).digest("hex");
}
export default new Service();
//# sourceMappingURL=KubernetesClusterService.js.map