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 { SQL } from "../Utils/AnalyticsDatabase/Statement";
import TelemetryType from "../../Types/Telemetry/TelemetryType";
import LogDatabaseService from "./LogService";
import MetricDatabaseService from "./MetricService";
import SpanDatabaseService from "./SpanService";
import ExceptionInstanceService from "./ExceptionInstanceService";
import TableColumnType from "../../Types/AnalyticsDatabase/TableColumnType";
import OneUptimeDate from "../../Types/Date";
import GlobalCache from "../Infrastructure/GlobalCache";
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
export class TelemetryAttributeService {
    getTelemetrySource(telemetryType) {
        switch (telemetryType) {
            case TelemetryType.Log:
                return {
                    service: LogDatabaseService,
                    tableName: LogDatabaseService.model.tableName,
                    attributesColumn: "attributes",
                    attributeKeysColumn: "attributeKeys",
                    timeColumn: "time",
                };
            case TelemetryType.Metric:
                return {
                    service: MetricDatabaseService,
                    tableName: MetricDatabaseService.model.tableName,
                    attributesColumn: "attributes",
                    attributeKeysColumn: "attributeKeys",
                    timeColumn: "time",
                };
            case TelemetryType.Trace:
                return {
                    service: SpanDatabaseService,
                    tableName: SpanDatabaseService.model.tableName,
                    attributesColumn: "attributes",
                    attributeKeysColumn: "attributeKeys",
                    timeColumn: "startTime",
                };
            case TelemetryType.Exception:
                return {
                    service: ExceptionInstanceService,
                    tableName: ExceptionInstanceService.model.tableName,
                    attributesColumn: "attributes",
                    // ExceptionInstance has no attributeKeys column.
                    timeColumn: "time",
                };
            default:
                return null;
        }
    }
    async fetchAttributes(data) {
        const source = this.getTelemetrySource(data.telemetryType);
        if (!source) {
            return [];
        }
        const cacheKey = TelemetryAttributeService.getCacheKey(data.projectId, data.telemetryType, data.metricName);
        const cachedEntry = await TelemetryAttributeService.getCachedAttributes(cacheKey);
        if (cachedEntry && TelemetryAttributeService.isCacheFresh(cachedEntry)) {
            return cachedEntry.attributes;
        }
        let attributes = [];
        try {
            attributes = await TelemetryAttributeService.fetchAttributesFromDatabase({
                projectId: data.projectId,
                source,
                metricName: data.metricName,
            });
        }
        catch (error) {
            if (cachedEntry) {
                return cachedEntry.attributes;
            }
            throw error;
        }
        await TelemetryAttributeService.storeAttributesInCache(cacheKey, attributes);
        if (attributes.length === 0 && cachedEntry) {
            return cachedEntry.attributes;
        }
        return attributes;
    }
    static getCacheKey(projectId, telemetryType, metricName) {
        const base = `${projectId.toString()}:${telemetryType}`;
        if (metricName) {
            return `${base}:${metricName}`;
        }
        return base;
    }
    static getLookbackStartDate() {
        return OneUptimeDate.addRemoveDays(OneUptimeDate.getCurrentDate(), -TelemetryAttributeService.LOOKBACK_WINDOW_IN_DAYS);
    }
    static async getCachedAttributes(cacheKey) {
        let payload = null;
        try {
            payload = await GlobalCache.getJSONObject(TelemetryAttributeService.CACHE_NAMESPACE, cacheKey);
        }
        catch (_a) {
            return null;
        }
        if (!payload) {
            return null;
        }
        const attributesValue = payload["attributes"];
        const refreshedAtValue = payload["refreshedAt"];
        if (!Array.isArray(attributesValue) ||
            typeof refreshedAtValue !== "string") {
            return null;
        }
        const attributeCandidates = attributesValue;
        const attributes = attributeCandidates.filter((attribute) => {
            return typeof attribute === "string";
        });
        return {
            attributes,
            refreshedAt: OneUptimeDate.fromString(refreshedAtValue),
        };
    }
    static isCacheFresh(cacheEntry) {
        const now = OneUptimeDate.getCurrentDate();
        const minutesSinceRefresh = Math.abs(OneUptimeDate.getNumberOfMinutesBetweenDates(cacheEntry.refreshedAt, now));
        return (minutesSinceRefresh <= TelemetryAttributeService.CACHE_STALE_AFTER_MINUTES);
    }
    static async storeAttributesInCache(cacheKey, attributes) {
        const payload = {
            attributes,
            refreshedAt: OneUptimeDate.getCurrentDate().toISOString(),
        };
        try {
            await GlobalCache.setJSON(TelemetryAttributeService.CACHE_NAMESPACE, cacheKey, payload, {
                expiresInSeconds: TelemetryAttributeService.CACHE_STALE_AFTER_MINUTES * 60,
            });
        }
        catch (_a) {
            return;
        }
    }
    static buildAttributesStatement(data) {
        const lookbackStartDate = TelemetryAttributeService.getLookbackStartDate();
        /*
         * Two notable choices here:
         *
         * 1. We aggregate with `groupUniqArrayArray` (or `mapKeys`+`groupUniqArray`
         *    for tables that lack the denormalized array column) instead of
         *    `arrayJoin` + outer `DISTINCT`. That avoids materializing one row
         *    per attribute key across millions of source rows.
         *
         * 2. The previous implementation wrapped the scan in
         *    `ORDER BY time DESC LIMIT 10000` to "cap" the work. With `arrayJoin`
         *    that LIMIT applied AFTER expansion so it didn't actually bound rows
         *    read, but it did force ClickHouse to sort every matching row by
         *    time — the dominant cost on busy projects. Bounded by lookback
         *    instead, the aggregate-and-flatten approach finishes in seconds.
         */
        const statement = data.attributeKeysColumn
            ? SQL `
      SELECT arrayDistinct(arrayFlatten(groupUniqArrayArray(${data.attributeKeysColumn}))) AS keys
      FROM ${data.tableName}
      WHERE projectId = ${{
                type: TableColumnType.ObjectID,
                value: data.projectId,
            }}
        AND NOT empty(${data.attributeKeysColumn})
        AND ${data.timeColumn} >= ${{
                type: TableColumnType.Date,
                value: lookbackStartDate,
            }}`
            : SQL `
      SELECT groupUniqArray(arrayJoin(mapKeys(${data.attributesColumn}))) AS keys
      FROM ${data.tableName}
      WHERE projectId = ${{
                type: TableColumnType.ObjectID,
                value: data.projectId,
            }}
        AND NOT empty(${data.attributesColumn})
        AND ${data.timeColumn} >= ${{
                type: TableColumnType.Date,
                value: lookbackStartDate,
            }}`;
        if (data.metricName) {
            statement.append(SQL `
        AND name = ${{
                type: TableColumnType.Text,
                value: data.metricName,
            }}`);
        }
        /*
         * Cap runtime below the ClickHouse client's 58s request_timeout so a
         * slow scan on a large project can't hold a pool connection for the
         * full timeout. 'break' returns partial keys, which is fine for an
         * attribute-key picker (matches the findBy / aggregation read paths).
         */
        statement.append(" SETTINGS max_execution_time = 45, timeout_overflow_mode = 'break'");
        return statement;
    }
    static async fetchAttributesFromDatabase(data) {
        const statement = TelemetryAttributeService.buildAttributesStatement({
            projectId: data.projectId,
            tableName: data.source.tableName,
            attributesColumn: data.source.attributesColumn,
            attributeKeysColumn: data.source.attributeKeysColumn,
            timeColumn: data.source.timeColumn,
            metricName: data.metricName,
        });
        const dbResult = await data.source.service.executeQuery(statement);
        const response = await dbResult.json();
        const rows = response.data || [];
        const firstRow = rows[0];
        const rawKeys = firstRow ? firstRow["keys"] : null;
        if (!Array.isArray(rawKeys)) {
            return [];
        }
        const attributeKeys = rawKeys
            .map((attribute) => {
            return typeof attribute === "string" ? attribute.trim() : null;
        })
            .filter((attribute) => {
            return Boolean(attribute);
        });
        const distinctKeys = Array.from(new Set(attributeKeys));
        distinctKeys.sort((a, b) => {
            return a.localeCompare(b);
        });
        if (distinctKeys.length > TelemetryAttributeService.ATTRIBUTES_LIMIT) {
            distinctKeys.length = TelemetryAttributeService.ATTRIBUTES_LIMIT;
        }
        return distinctKeys;
    }
    async fetchAttributeValues(data) {
        const source = this.getTelemetrySource(data.telemetryType);
        if (!source) {
            return [];
        }
        return TelemetryAttributeService.fetchAttributeValuesFromDatabase({
            projectId: data.projectId,
            source,
            metricName: data.metricName,
            attributeKey: data.attributeKey,
            searchText: data.searchText,
        });
    }
    static buildAttributeValuesStatement(data) {
        const lookbackStartDate = TelemetryAttributeService.getLookbackStartDate();
        const statement = SQL `
      SELECT DISTINCT ${data.source.attributesColumn}[${{
            type: TableColumnType.Text,
            value: data.attributeKey,
        }}] AS attributeValue
      FROM ${data.source.tableName}
      WHERE projectId = ${{
            type: TableColumnType.ObjectID,
            value: data.projectId,
        }}
        AND ${data.source.timeColumn} >= ${{
            type: TableColumnType.Date,
            value: lookbackStartDate,
        }}
        AND mapContains(${data.source.attributesColumn}, ${{
            type: TableColumnType.Text,
            value: data.attributeKey,
        }})`;
        if (data.metricName) {
            statement.append(SQL `
        AND name = ${{
                type: TableColumnType.Text,
                value: data.metricName,
            }}`);
        }
        /*
         * Case-insensitive substring filter so the value autocomplete keeps
         * narrowing server-side as the user types. Without it only the first
         * ATTRIBUTE_VALUES_LIMIT values (alphabetically) are ever reachable,
         * which hides matches on high-cardinality keys (host.name, url, ...).
         * Mirrors the ILIKE idiom used for bodySearchText / nameSearchText.
         */
        if (data.searchText && data.searchText.trim().length > 0) {
            statement.append(SQL `
        AND ${data.source.attributesColumn}[${{
                type: TableColumnType.Text,
                value: data.attributeKey,
            }}] ILIKE ${{
                type: TableColumnType.Text,
                value: `%${data.searchText.trim()}%`,
            }}`);
        }
        statement.append(SQL `
      ORDER BY attributeValue ASC
      LIMIT ${{
            type: TableColumnType.Number,
            value: TelemetryAttributeService.ATTRIBUTE_VALUES_LIMIT,
        }}`);
        /*
         * Cap runtime below the client's 58s request_timeout. This value
         * autocomplete runs per keystroke and scans a Map subscript, so a
         * pathological key/project must not hold a pool connection; 'break'
         * returns partial values, acceptable for autocomplete.
         */
        statement.append(" SETTINGS max_execution_time = 45, timeout_overflow_mode = 'break'");
        return statement;
    }
    static async fetchAttributeValuesFromDatabase(data) {
        const statement = TelemetryAttributeService.buildAttributeValuesStatement(data);
        const dbResult = await data.source.service.executeQuery(statement);
        const response = await dbResult.json();
        const rows = response.data || [];
        return rows
            .map((row) => {
            const val = row["attributeValue"];
            return typeof val === "string" ? val.trim() : null;
        })
            .filter((val) => {
            return Boolean(val);
        });
    }
}
TelemetryAttributeService.ATTRIBUTES_LIMIT = 5000;
TelemetryAttributeService.CACHE_NAMESPACE = "telemetry-attributes";
/*
 * Attribute keys change rarely. Cache for an hour so the (still O(seconds))
 * ClickHouse scan only runs once per project per hour rather than on every
 * dashboard / metrics-explorer load.
 */
TelemetryAttributeService.CACHE_STALE_AFTER_MINUTES = 60;
/*
 * The previous 30-day window forced a 100M+ row scan with an in-CTE
 * ORDER BY time DESC that pushed this query to 30-60s on busy projects.
 * Attribute keys rotate slowly, so a 1-day window covers virtually every
 * active key while keeping the scan tractable.
 */
TelemetryAttributeService.LOOKBACK_WINDOW_IN_DAYS = 1;
TelemetryAttributeService.ATTRIBUTE_VALUES_LIMIT = 100;
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], TelemetryAttributeService.prototype, "fetchAttributes", null);
__decorate([
    CaptureSpan(),
    __metadata("design:type", Function),
    __metadata("design:paramtypes", [Object]),
    __metadata("design:returntype", Promise)
], TelemetryAttributeService.prototype, "fetchAttributeValues", null);
export default new TelemetryAttributeService();
//# sourceMappingURL=TelemetryAttributeService.js.map