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 HostLabelRuleEngineService from "./HostLabelRuleEngineService";
import HostOwnerRuleEngineService from "./HostOwnerRuleEngineService";
import Model from "../../Models/DatabaseModels/Host";
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 { canonicalizeEntityValue } from "../../Utils/Telemetry/EntityKey";
import crypto from "crypto";
const LAST_SEEN_CACHE_NAMESPACE = "host-last-seen";
const LAST_SEEN_THROTTLE_SECONDS = 60;
const LABELS_APPLIED_CACHE_NAMESPACE = "host-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) {
            /*
             * Run label rule first so rule-added labels are persisted before
             * owner rules run. Owner rules re-fetch labels, so this lets owner
             * rules key on rule-added labels.
             */
            Promise.resolve()
                .then(async () => {
                await HostLabelRuleEngineService.applyRulesToHost(createdItem);
            })
                .then(async () => {
                await HostOwnerRuleEngineService.applyRulesToHost(createdItem);
            })
                .catch((error) => {
                var _a, _b;
                logger.error(`Error applying host rules in HostService.onCreateSuccess: ${error}`, {
                    projectId: (_a = createdItem.projectId) === null || _a === void 0 ? void 0 : _a.toString(),
                    hostId: (_b = createdItem.id) === null || _b === void 0 ? void 0 : _b.toString(),
                });
            });
        }
        return createdItem;
    }
    async findOrCreateByHostIdentifier(data) {
        /*
         * Canonicalize the identifier (trim + lowercase, matching
         * QueryHelper.findWithSameText). Host identity comes from the OTel
         * `host.name` resource attribute, whose casing is not stable across
         * batches — Windows in particular surfaces the hostname uppercased
         * (`COMPUTERNAME`-style, e.g. PRIMARY01) from some resource detectors
         * and lowercased from others, so the same physical host arrives as
         * both `PRIMARY01` and `primary01`. Ingest already canonicalizes
         * host.name (OtelIngestBaseService.normalizeHostNameAttributesInPlace);
         * we repeat it here so the method is correct for any caller.
         */
        const hostIdentifier = canonicalizeEntityValue(data.hostIdentifier);
        /*
         * Look up case-insensitively. The unique guard on name/hostIdentifier
         * (DatabaseService.checkUniqueColumnBy -> QueryHelper.findWithSameText)
         * already compares case-insensitively, so a case-sensitive lookup here
         * would miss an existing row, then fail to create it ("Host with the
         * same name already exists"), and permanently wedge ingest for that
         * host. Mirrors LabelService.findOrCreateLabelByName.
         */
        const existingHost = await this.findOneBy({
            query: {
                projectId: data.projectId,
                hostIdentifier: QueryHelper.findWithSameText(hostIdentifier),
            },
            select: {
                _id: true,
                projectId: true,
                hostIdentifier: true,
            },
            props: {
                isRoot: true,
            },
        });
        if (existingHost) {
            /*
             * Converge a legacy mixed-case identifier onto the canonical form so
             * the stored resource.host.name (also canonicalized at ingest) keeps
             * matching the host-detail page filter. Best-effort — never block
             * ingest on it. Updates don't re-run the unique guard, so writing the
             * host's own canonical identifier cannot collide.
             */
            if (existingHost._id &&
                existingHost.hostIdentifier &&
                existingHost.hostIdentifier !== hostIdentifier) {
                try {
                    await this.updateOneById({
                        id: new ObjectID(existingHost._id.toString()),
                        data: {
                            hostIdentifier: hostIdentifier,
                        },
                        props: {
                            isRoot: true,
                        },
                    });
                    existingHost.hostIdentifier = hostIdentifier;
                }
                catch (err) {
                    logger.warn(`HostService: failed to canonicalize hostIdentifier for host ${existingHost._id.toString()}: ${err instanceof Error ? err.message : String(err)}`);
                }
            }
            return existingHost;
        }
        try {
            const newHost = new Model();
            newHost.projectId = data.projectId;
            newHost.name = hostIdentifier;
            newHost.hostIdentifier = hostIdentifier;
            newHost.otelCollectorStatus = "connected";
            newHost.lastSeenAt = OneUptimeDate.getCurrentDate();
            const createdHost = await this.create({
                data: newHost,
                props: {
                    isRoot: true,
                },
            });
            return createdHost;
        }
        catch (_a) {
            /*
             * Either two ingest workers raced to create the same host, or a
             * host with this identifier in a different case already existed and
             * the unique guard rejected the insert. Re-resolve case-insensitively
             * so the caller still gets the existing row instead of throwing.
             */
            const reFetchedHost = await this.findOneBy({
                query: {
                    projectId: data.projectId,
                    hostIdentifier: QueryHelper.findWithSameText(hostIdentifier),
                },
                select: {
                    _id: true,
                    projectId: true,
                    hostIdentifier: true,
                },
                props: {
                    isRoot: true,
                },
            });
            if (reFetchedHost) {
                return reFetchedHost;
            }
            throw new Error("Failed to create or find host: " + hostIdentifier);
        }
    }
    async updateLastSeen(hostId, extra) {
        /*
         * Throttle: the same telemetry batch repeats every metric/log/trace
         * push and re-sends identical host metadata. Skip the DB write when
         * we recently wrote the exact same values; only refresh `lastSeenAt`
         * once per throttle window. If any extra value changed (e.g. cpuCores
         * updated, IP address changed), bust the cache and write immediately.
         */
        const cacheKey = hostId.toString();
        const extrasFingerprint = this.fingerprintExtras(extra);
        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.osType) {
            data.osType = extra.osType;
        }
        if (extra === null || extra === void 0 ? void 0 : extra.osVersion) {
            data.osVersion = extra.osVersion;
        }
        if (extra === null || extra === void 0 ? void 0 : extra.hostId) {
            data.hostId = extra.hostId;
        }
        if (extra === null || extra === void 0 ? void 0 : extra.hostArch) {
            data.hostArch = extra.hostArch;
        }
        if (extra === null || extra === void 0 ? void 0 : extra.hostType) {
            data.hostType = extra.hostType;
        }
        if (extra === null || extra === void 0 ? void 0 : extra.hostIpAddresses) {
            data.hostIpAddresses = extra.hostIpAddresses;
        }
        if ((extra === null || extra === void 0 ? void 0 : extra.cpuCores) !== undefined) {
            data.cpuCores = extra.cpuCores;
        }
        if ((extra === null || extra === void 0 ? void 0 : extra.totalMemoryBytes) !== undefined) {
            data.totalMemoryBytes = extra.totalMemoryBytes;
        }
        if ((extra === null || extra === void 0 ? void 0 : extra.processCount) !== undefined) {
            data.processCount = extra.processCount;
        }
        if (extra === null || extra === void 0 ? void 0 : extra.containerRuntime) {
            data.containerRuntime = extra.containerRuntime;
        }
        if (extra === null || extra === void 0 ? void 0 : extra.dockerHostId) {
            data.dockerHostId = extra.dockerHostId;
        }
        if (extra === null || extra === void 0 ? void 0 : extra.kubernetesClusterId) {
            data.kubernetesClusterId = extra.kubernetesClusterId;
        }
        if (extra === null || extra === void 0 ? void 0 : extra.agentVersion) {
            data.agentVersion = extra.agentVersion;
        }
        if (extra === null || extra === void 0 ? void 0 : extra.deploymentEnvironment) {
            data.deploymentEnvironment = extra.deploymentEnvironment;
        }
        if (extra === null || extra === void 0 ? void 0 : extra.runtimeName) {
            data.runtimeName = extra.runtimeName;
        }
        if (extra === null || extra === void 0 ? void 0 : extra.runtimeVersion) {
            data.runtimeVersion = extra.runtimeVersion;
        }
        if (extra === null || extra === void 0 ? void 0 : extra.cloudProvider) {
            data.cloudProvider = extra.cloudProvider;
        }
        if (extra === null || extra === void 0 ? void 0 : extra.cloudPlatform) {
            data.cloudPlatform = extra.cloudPlatform;
        }
        if (extra === null || extra === void 0 ? void 0 : extra.cloudRegion) {
            data.cloudRegion = extra.cloudRegion;
        }
        if (extra === null || extra === void 0 ? void 0 : extra.cloudAccountId) {
            data.cloudAccountId = extra.cloudAccountId;
        }
        await this.updateOneById({
            id: hostId,
            data: data,
            props: {
                isRoot: true,
            },
        });
    }
    fingerprintExtras(extra) {
        var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x;
        const normalized = {
            osType: (_a = extra === null || extra === void 0 ? void 0 : extra.osType) !== null && _a !== void 0 ? _a : null,
            osVersion: (_b = extra === null || extra === void 0 ? void 0 : extra.osVersion) !== null && _b !== void 0 ? _b : null,
            hostId: (_c = extra === null || extra === void 0 ? void 0 : extra.hostId) !== null && _c !== void 0 ? _c : null,
            hostArch: (_d = extra === null || extra === void 0 ? void 0 : extra.hostArch) !== null && _d !== void 0 ? _d : null,
            hostType: (_e = extra === null || extra === void 0 ? void 0 : extra.hostType) !== null && _e !== void 0 ? _e : null,
            hostIpAddresses: (_f = extra === null || extra === void 0 ? void 0 : extra.hostIpAddresses) !== null && _f !== void 0 ? _f : null,
            cpuCores: (_g = extra === null || extra === void 0 ? void 0 : extra.cpuCores) !== null && _g !== void 0 ? _g : null,
            totalMemoryBytes: (_h = extra === null || extra === void 0 ? void 0 : extra.totalMemoryBytes) !== null && _h !== void 0 ? _h : null,
            processCount: (_j = extra === null || extra === void 0 ? void 0 : extra.processCount) !== null && _j !== void 0 ? _j : null,
            containerRuntime: (_k = extra === null || extra === void 0 ? void 0 : extra.containerRuntime) !== null && _k !== void 0 ? _k : null,
            dockerHostId: (_m = (_l = extra === null || extra === void 0 ? void 0 : extra.dockerHostId) === null || _l === void 0 ? void 0 : _l.toString()) !== null && _m !== void 0 ? _m : null,
            kubernetesClusterId: (_p = (_o = extra === null || extra === void 0 ? void 0 : extra.kubernetesClusterId) === null || _o === void 0 ? void 0 : _o.toString()) !== null && _p !== void 0 ? _p : null,
            agentVersion: (_q = extra === null || extra === void 0 ? void 0 : extra.agentVersion) !== null && _q !== void 0 ? _q : null,
            deploymentEnvironment: (_r = extra === null || extra === void 0 ? void 0 : extra.deploymentEnvironment) !== null && _r !== void 0 ? _r : null,
            runtimeName: (_s = extra === null || extra === void 0 ? void 0 : extra.runtimeName) !== null && _s !== void 0 ? _s : null,
            runtimeVersion: (_t = extra === null || extra === void 0 ? void 0 : extra.runtimeVersion) !== null && _t !== void 0 ? _t : null,
            cloudProvider: (_u = extra === null || extra === void 0 ? void 0 : extra.cloudProvider) !== null && _u !== void 0 ? _u : null,
            cloudPlatform: (_v = extra === null || extra === void 0 ? void 0 : extra.cloudPlatform) !== null && _v !== void 0 ? _v : null,
            cloudRegion: (_w = extra === null || extra === void 0 ? void 0 : extra.cloudRegion) !== null && _w !== void 0 ? _w : null,
            cloudAccountId: (_x = extra === null || extra === void 0 ? void 0 : extra.cloudAccountId) !== null && _x !== void 0 ? _x : null,
        };
        return crypto
            .createHash("sha1")
            .update(JSON.stringify(normalized))
            .digest("hex");
    }
    /**
     * Additively attach labels to a host. 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.hostId.toString();
        const fingerprint = fingerprintLabelIds(data.labelIds);
        const cached = await GlobalCache.getString(LABELS_APPLIED_CACHE_NAMESPACE, cacheKey);
        if (cached === fingerprint) {
            return;
        }
        try {
            const hostIdStr = data.hostId.toString();
            const existingLabels = await this.getRepository()
                .createQueryBuilder()
                .relation(Model, "labels")
                .of(hostIdStr)
                .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(hostIdStr)
                    .add(toAddIds);
            }
            await GlobalCache.setString(LABELS_APPLIED_CACHE_NAMESPACE, cacheKey, fingerprint, { expiresInSeconds: LABELS_APPLIED_CACHE_TTL_SECONDS });
        }
        catch (err) {
            /*
             * A concurrent ingest worker may have inserted the same join
             * row between our loadMany and add. Best-effort — surface as
             * a warning so chronic failures show up in logs without
             * breaking ingest.
             */
            logger.warn(`HostService.attachLabels failed for host ${data.hostId.toString()}: ${err instanceof Error ? err.message : String(err)}`);
        }
    }
    async markDisconnectedHosts() {
        const fiveMinutesAgo = OneUptimeDate.addRemoveMinutes(OneUptimeDate.getCurrentDate(), -5);
        const connectedHosts = await this.findBy({
            query: {
                otelCollectorStatus: "connected",
                lastSeenAt: QueryHelper.lessThan(fiveMinutesAgo),
            },
            select: {
                _id: true,
            },
            limit: LIMIT_MAX,
            skip: 0,
            props: {
                isRoot: true,
            },
        });
        for (const host of connectedHosts) {
            if (host._id) {
                await this.updateOneById({
                    id: new ObjectID(host._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, "findOrCreateByHostIdentifier", 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, "markDisconnectedHosts", 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=HostService.js.map