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 Model from "../../Models/DatabaseModels/TelemetryEntity";
import OneUptimeDate from "../../Types/Date";
import ColumnLength from "../../Types/Database/ColumnLength";
import logger from "../Utils/Logger";
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
import { getEntityBudget, reconcileByNaturalKey, REGISTRY_PROMOTED_TYPES, shouldWarnEntityBudgetOnce, } from "../Utils/Telemetry/EntityRegistry";
export class TelemetryEntityService extends DatabaseService {
    constructor() {
        super(Model);
    }
    /**
     * Forward-only registry reconciliation: upsert a catalog row per
     * discovered entity, bump `lastSeenAt`, merge descriptive attributes
     * (last-writer-wins) and union labels. High-churn membership-only
     * types (container / process / service.instance / telemetry.sdk — see
     * `REGISTRY_PROMOTED_TYPES`) are skipped: their keys still flow into
     * the `entityKeys` column on signals, they just never mint registry
     * rows. New rows are additionally gated by a per-(project, type)
     * entity budget. Resilient by design — a registry failure must never break
     * signal ingest, so every error is swallowed (logged). Callers are
     * expected to throttle this (it should not run on every batch); see
     * `reconcileEntityRegistryThrottled` in
     * `Common/Server/Utils/Telemetry/EntityRegistry`.
     */
    async reconcileEntities(data) {
        /*
         * Per-reconcile budget count cache: at most one countBy per entity
         * type per reconcile (the fence already throttles reconciles to one
         * per entity-set per window, so this stays cheap). Creates within
         * this reconcile increment the cached count so a single batch cannot
         * blow past the budget.
         */
        const countByType = new Map();
        for (const entity of data.entities) {
            if (!REGISTRY_PROMOTED_TYPES.has(entity.entityType)) {
                continue;
            }
            try {
                await this.upsertEntity({
                    projectId: data.projectId,
                    entity,
                    countByType,
                });
            }
            catch (err) {
                logger.error(`TelemetryEntityService: failed to upsert entity ${entity.entityType}/${entity.entityKey}:`);
                logger.error(err);
            }
        }
    }
    async upsertEntity(data) {
        const { projectId, entity, countByType } = data;
        const now = OneUptimeDate.getCurrentDate();
        await reconcileByNaturalKey({
            service: this,
            query: {
                projectId,
                entityType: entity.entityType,
                entityKey: entity.entityKey,
            },
            lastSeenAt: now,
            describe: `entity ${entity.entityType}/${entity.entityKey}`,
            select: { descriptiveAttributes: true, labels: true },
            buildUpdate: (existing) => {
                return TelemetryEntityService.buildDescriptiveUpdate(entity, existing);
            },
            /*
             * Entity budget (doc §Edge Cases): existing rows always get their
             * bump above; only NEW rows are gated. Counted via the
             * (projectId, entityType) index; over budget the row is skipped —
             * its key still flows in `entityKeys` on signals.
             */
            beforeCreate: async () => {
                const budget = getEntityBudget(entity.entityType);
                let count = countByType.get(entity.entityType);
                if (count === undefined) {
                    count = (await this.countBy({
                        query: { projectId, entityType: entity.entityType },
                        props: { isRoot: true },
                    })).toNumber();
                    countByType.set(entity.entityType, count);
                }
                if (count < budget) {
                    countByType.set(entity.entityType, count + 1);
                    return true;
                }
                if (await shouldWarnEntityBudgetOnce({
                    projectId,
                    entityType: entity.entityType,
                })) {
                    logger.warn(`TelemetryEntityService: entity budget reached for project ${projectId.toString()} type ${entity.entityType} (${count} >= ${budget}); skipping new registry rows. Membership keys still flow on signals.`);
                }
                return false;
            },
            buildModel: () => {
                const model = new Model();
                model.projectId = projectId;
                model.entityType = entity.entityType;
                model.entityKey = entity.entityKey;
                model.identifyingAttributes = entity.identifyingAttributes;
                if (entity.descriptiveAttributes &&
                    Object.keys(entity.descriptiveAttributes).length > 0) {
                    model.descriptiveAttributes = entity.descriptiveAttributes;
                }
                if (entity.labels && entity.labels.length > 0) {
                    model.labels = TelemetryEntityService.normalizeLabels(entity.labels);
                }
                // varchar(ShortText) column; k8s names can run to 253 chars.
                model.displayName = TelemetryEntityService.deriveDisplayName(entity).substring(0, ColumnLength.ShortText);
                model.firstSeenAt = now;
                model.lastSeenAt = now;
                return model;
            },
        });
    }
    /**
     * Extra fields for the `lastSeenAt` bump on an existing row:
     * descriptive attributes merge last-writer-wins per key, labels merge
     * as a set union. Both are written ONLY when the merge actually changes
     * the stored value, so a stable resource keeps its bump a one-column
     * update (no jsonb churn).
     */
    static buildDescriptiveUpdate(entity, existing) {
        /*
         * Built as a plain shape and cast once on return: assigning into
         * QueryDeepPartialEntity<Model> directly forces TS to instantiate the
         * recursive JSONObject mapping (TS2589).
         */
        const update = {};
        const incoming = entity.descriptiveAttributes || {};
        if (Object.keys(incoming).length > 0) {
            const current = existing.descriptiveAttributes || {};
            const differs = Object.keys(incoming).some((key) => {
                return current[key] !== incoming[key];
            });
            if (differs) {
                update.descriptiveAttributes = Object.assign(Object.assign({}, current), incoming);
            }
        }
        const incomingLabels = entity.labels || [];
        if (incomingLabels.length > 0) {
            const currentLabels = existing.labels || [];
            const currentSet = new Set(currentLabels);
            const hasNew = incomingLabels.some((label) => {
                return !currentSet.has(label);
            });
            if (hasNew) {
                update.labels = TelemetryEntityService.normalizeLabels([
                    ...currentLabels,
                    ...incomingLabels,
                ]);
            }
        }
        return update;
    }
    /** Sorted + deduped for a stable stored value. */
    static normalizeLabels(labels) {
        return Array.from(new Set(labels)).sort();
    }
    /**
     * Best-effort human-readable name for the entity explorer: prefer the
     * most specific `*.name` identifying attribute (e.g. k8s.pod.name over
     * k8s.cluster.name), else the last identifying value, else the key.
     */
    static deriveDisplayName(entity) {
        const id = entity.identifyingAttributes || {};
        const nameKeys = Object.keys(id)
            .filter((k) => {
            return k.endsWith(".name");
        })
            .sort((a, b) => {
            return b.length - a.length;
        });
        if (nameKeys.length > 0 && id[nameKeys[0]]) {
            return id[nameKeys[0]];
        }
        const keys = Object.keys(id);
        if (keys.length > 0 && id[keys[keys.length - 1]]) {
            return id[keys[keys.length - 1]];
        }
        return entity.entityKey;
    }
}
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], TelemetryEntityService.prototype, "reconcileEntities", null);
export default new TelemetryEntityService();
//# sourceMappingURL=TelemetryEntityService.js.map