"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getOp = getOp;
exports.get = get;
exports.listMetricsOp = listMetricsOp;
exports.listMetrics = listMetrics;
exports.describeMetricOp = describeMetricOp;
exports.describeMetric = describeMetric;
const harper_logger_ts_1 = __importDefault(require("../../utility/logging/harper_logger.js"));
const { forComponent } = harper_logger_ts_1.default;
const hostnames_ts_1 = require("./hostnames.js");
const metadata_ts_1 = require("./metadata.js");
const hdbTerms_ts_1 = require("../../utility/hdbTerms.js");
const environmentManager_ts_1 = require("../../utility/environment/environmentManager.js");
const analyticsValidator_ts_1 = require("../../validation/analyticsValidator.js");
const hdbError_ts_1 = require("../../utility/errors/hdbError.js");
const nodeName_ts_1 = require("../../server/nodeName.js");
// default to one week time window for finding custom metrics
const defaultCustomMetricWindow = 1000 * 60 * 60 * 24 * 7;
const logger = forComponent('analytics');
const log = logger.conditional;
async function lookupHostname(nodeId) {
    const result = await (0, hostnames_ts_1.getAnalyticsHostnameTable)().get(nodeId);
    if (result?.hostname)
        return result.hostname;
    if (nodeId === (0, hostnames_ts_1.stableNodeId)(server.hostname))
        return server.hostname;
    return undefined;
}
function isSelected(querySelect, attr) {
    return querySelect.length === 0 || querySelect.includes(attr);
}
/**
 * Validates the `get_analytics` request and returns the analytics results.
 * @param req
 * @returns
 */
async function getOp(req) {
    log.trace?.('get_analytics request:', req);
    const validationError = (0, analyticsValidator_ts_1.validateGetAnalytics)(req);
    if (validationError) {
        throw (0, hdbError_ts_1.handleHDBError)(validationError, validationError.message, hdbError_ts_1.hdbErrors.HTTP_STATUS_CODES.BAD_REQUEST, undefined, undefined, true);
    }
    // `replicated` fans the query out to every peer node and merges each node's
    // analytics into one cluster-wide result set. Fan-out is skipped when:
    //  - the request did not ask for it;
    //  - this is standalone core, which has no `server.nodes` (harper-pro populates it);
    //  - `hdb_analytics` already replicates across the cluster, in which case a local
    //    query already holds every node's rows and fanning out would double-count.
    //    The DB layer marks the table `replicate === false` only when it is *not*
    //    replicated (`analytics_replicate: false`), which is exactly when fan-out helps.
    const analyticsReplicatedByDb = databases.system.hdb_analytics.replicate !== false;
    const peers = req.replicated && !analyticsReplicatedByDb && server.nodes?.length ? server.nodes : undefined;
    // When merging across the cluster, make sure every row keeps its origin `node`
    // attribute so callers can tell the nodes apart. An empty/absent `get_attributes`
    // already selects everything (including `node`), so only an explicit list needs it.
    let getAttributes = req.get_attributes;
    if (peers && getAttributes?.length && !getAttributes.includes('node')) {
        getAttributes = [...getAttributes, 'node'];
    }
    const localResults = await get(req.metric, {
        getAttributes,
        startTime: req.start_time,
        endTime: req.end_time,
        coalesceTime: req.coalesce_time,
        additionalConditions: req.conditions,
    });
    if (!peers)
        return localResults;
    return mergeAnalyticsFromPeers(localResults, { ...req, get_attributes: getAttributes }, peers);
}
/**
 * Streams the local analytics, then appends each peer node's analytics. The same
 * query is forwarded to every peer with `replicated` cleared so each returns only
 * its own local metrics (no recursive fan-out). Best-effort: a peer that fails is
 * logged and omitted rather than failing the whole query.
 */
async function* mergeAnalyticsFromPeers(localResults, req, peers) {
    const thisNode = (0, nodeName_ts_1.getThisNodeName)();
    const peerReq = { ...req, replicated: false };
    // `sendOperationToNode` is typed for a node name string, but the replication
    // implementation expects the full node object (the stub-vs-impl mismatch that
    // `restart` works around the same way).
    const sendOperationToNode = server.replication.sendOperationToNode;
    const peerResults = peers
        .filter((node) => node.name !== thisNode)
        .map((node) => sendOperationToNode(node, peerReq).then(
    // an array response is wrapped as `{ results }` over the replication channel;
    // fall back to an empty list for any other (malformed) shape so a bad peer
    // can't break the merge
    (response) => Array.isArray(response) ? response : Array.isArray(response?.results) ? response.results : [], (error) => {
        logger.warn(`get_analytics replication to node '${node.name}' failed; omitting its results`, error);
        return [];
    }));
    yield* localResults;
    for (const peerResult of peerResults) {
        yield* await peerResult;
    }
}
function conformCondition(condition) {
    if ('conditions' in condition) {
        return {
            ...condition,
            conditions: condition.conditions.map(conformCondition),
        };
    }
    return {
        attribute: condition.search_attribute ?? condition.attribute,
        comparator: condition.search_type ?? condition.comparator,
        value: condition.search_value ?? condition.value,
    };
}
async function* coalesceResults(results, window) {
    let coalesceId;
    for await (const result of results) {
        const id = result.id;
        if (!coalesceId) {
            coalesceId = id;
        }
        const delta = Math.abs(id - coalesceId);
        if (delta < window) {
            yield { ...result, id: coalesceId };
        }
        else {
            yield result;
            coalesceId = id;
        }
    }
}
async function get(metric, opts) {
    const { getAttributes, startTime, endTime, additionalConditions } = opts ?? {};
    const conditions = [{ attribute: 'metric', comparator: 'equals', value: metric }];
    if (additionalConditions) {
        conditions.push(...additionalConditions.map(conformCondition));
    }
    const select = getAttributes ?? [];
    // ensure we're always selecting id
    if (!isSelected(select, 'id')) {
        select.push('id');
    }
    if (startTime && endTime) {
        conditions.push({
            attribute: 'id',
            comparator: 'between',
            value: [startTime, endTime],
        });
    }
    else {
        if (startTime) {
            conditions.push({
                attribute: 'id',
                comparator: 'greater_than_equal',
                value: startTime,
            });
        }
        if (endTime) {
            conditions.push({
                attribute: 'id',
                comparator: 'less_than',
                value: endTime,
            });
        }
    }
    const request = { conditions, allowConditionsOnDynamicAttributes: true };
    if (select.length > 0) {
        request['select'] = select;
    }
    log.trace?.('get_analytics hdb_analytics.search request:', JSON.stringify(request));
    const searchResults = await databases.system.hdb_analytics.search(request);
    let results = searchResults.map(async (result) => {
        // remove nodeId from 'id' attr and resolve it to the actual hostname and
        // add back in as 'node' attr if selected
        const nodeId = result.id[1];
        result = { ...result, id: result.id[0] };
        if (isSelected(select, 'node')) {
            log.trace?.(`get_analytics lookup hostname for nodeId: ${nodeId}`);
            const hostname = await lookupHostname(nodeId);
            result['node'] = hostname ?? nodeId;
        }
        log.trace?.(`get_analytics result:`, JSON.stringify(result));
        return result;
    });
    if (opts?.coalesceTime) {
        // coalescing window is the aggregate period converted to milliseconds
        const window = (0, environmentManager_ts_1.get)(hdbTerms_ts_1.CONFIG_PARAMS.ANALYTICS_AGGREGATEPERIOD) * 1000;
        results = coalesceResults(results, window);
    }
    return results;
}
function listMetricsOp(req) {
    return listMetrics(req.metric_types, req.custom_metrics_window);
}
async function listMetrics(metricTypes = ['builtin'], customWindow = defaultCustomMetricWindow) {
    let metrics = [];
    const builtins = Object.values(metadata_ts_1.METRIC);
    if (metricTypes.includes('builtin')) {
        metrics = builtins;
    }
    if (metricTypes.includes('custom')) {
        const oldestCustomId = Date.now() - customWindow;
        const conditions = [
            {
                attribute: 'id',
                comparator: 'greater_than',
                value: oldestCustomId,
            },
        ];
        const metricConditions = builtins.map((c) => {
            return {
                attribute: 'metric',
                comparator: 'not_equal',
                value: c,
            };
        });
        conditions.push(...metricConditions);
        const customMetricsSearch = {
            select: ['metric'],
            conditions: conditions,
        };
        const customMetrics = new Set();
        const searchResults = await databases.system.hdb_analytics.search(customMetricsSearch);
        for await (const record of searchResults) {
            customMetrics.add(record.metric);
        }
        metrics.push(...Array.from(customMetrics.values()));
    }
    return metrics;
}
function describeMetricOp(req) {
    return describeMetric(req.metric);
}
async function describeMetric(metric) {
    const lastEntrySearch = {
        conditions: [{ attribute: 'metric', comparator: 'equals', value: metric }],
        sort: {
            attribute: 'id',
            descending: true,
        },
        limit: 1,
    };
    const results = databases.system.hdb_analytics.search(lastEntrySearch);
    // node is a synthetic attribute, so make sure it's included
    const attributes = [{ name: 'node', type: 'string' }];
    for await (const result of results) {
        for (const attr in result) {
            attributes.push({ name: attr, type: typeof result[attr] });
        }
        const desc = {
            attributes,
        };
        log.trace?.('describe_metric result:', JSON.stringify(desc));
        return desc;
    }
    // if no results, return empty object
    return {};
}
//# sourceMappingURL=read.js.map