/*
 * Copyright (c) 2025, Salesforce, Inc.
 * SPDX-License-Identifier: Apache-2
 * For full license text, see the license.txt file in the repo root or http://www.apache.org/licenses/LICENSE-2.0
 */
import { normalizeTenantId } from '../../clients/custom-apis.js';
import { getLogger } from '../../logging/logger.js';
/**
 * Maps a {@link MetricsTagContext} filter field to the tag key it authoritatively
 * sets when present. These override any value the string heuristics would infer.
 */
const FILTER_TAG_KEYS = [
    ['apiFamily', 'apiFamily'],
    ['apiName', 'apiName'],
    ['ocapiCategory', 'ocapiCategory'],
    ['ocapiApi', 'ocapiApi'],
    ['thirdPartyServiceId', 'thirdPartyServiceId'],
];
/**
 * Splits a normalized tenant id (`bdpx_prd`) into its realm and environment.
 * The environment is the final underscore-delimited segment; everything before
 * it is the realm. Ids without an underscore yield just a realm.
 */
function splitRealmEnvironment(tenantId) {
    const normalized = normalizeTenantId(tenantId);
    const lastUnderscore = normalized.lastIndexOf('_');
    if (lastUnderscore <= 0 || lastUnderscore === normalized.length - 1) {
        return { realm: normalized };
    }
    return {
        realm: normalized.slice(0, lastUnderscore),
        environment: normalized.slice(lastUnderscore + 1),
    };
}
/**
 * Strips a leading `realm.` or `realm ` prefix from a packed series id, if
 * present. The Metrics API prefixes most ids with the realm using either a `.`
 * or a space; this removes whichever is found so the category rule sees only the
 * dimension remainder. Returns the input unchanged when no realm prefix matches.
 */
function stripRealmPrefix(seriesId, realm) {
    if (seriesId.startsWith(`${realm}.`))
        return seriesId.slice(realm.length + 1);
    if (seriesId.startsWith(`${realm} `))
        return seriesId.slice(realm.length + 1);
    return seriesId;
}
/**
 * Extractor for series whose remainder is a bare API family (`product`,
 * `custom`, …) — but which may instead be an HTTP status class (`2xx`) or the
 * metric's own fallback id. SCAPI mixes these within a single metric.
 */
const scapiFamilyOrStatus = (remainder) => {
    if (/^[1-5]xx$/.test(remainder))
        return { statusClass: remainder };
    return { apiFamily: remainder };
};
/**
 * Per-category, per-metric extraction rules. Keyed by `category` then
 * `metricId`; a category-level `*` entry applies to any metric not explicitly
 * listed. Rules operate on the realm-stripped remainder (see
 * {@link stripRealmPrefix}); returning `{}` means "no extra dimensions, just the
 * context tags."
 */
const EXTRACTORS = {
    scapi: {
        // `bdpx.product` → apiFamily=product; `bdpx 2xx`/`bdpx 3xx` → statusClass
        totalCalls: scapiFamilyOrStatus,
        requestLatency: (remainder) => {
            // `Average overall latency` is a rollup, not a per-family series.
            if (/overall/i.test(remainder))
                return { aggregation: 'overall' };
            return { apiFamily: remainder };
        },
        responseCount: scapiFamilyOrStatus,
        errors4xx: (remainder) => ({ apiFamily: remainder }),
        // `bdpx.product HIT` / `bdpx.custom MISS` → apiFamily + cacheStatus
        cacheHitRate: (remainder) => {
            const spaceIdx = remainder.lastIndexOf(' ');
            if (spaceIdx > 0) {
                return { apiFamily: remainder.slice(0, spaceIdx), cacheStatus: remainder.slice(spaceIdx + 1) };
            }
            return { apiFamily: remainder };
        },
    },
    ocapi: {
        // `bdpx.shop` → ocapiCategory=shop
        totalCalls: (remainder) => ({ ocapiCategory: remainder }),
        callsMean: (remainder) => ({ ocapiCategory: remainder }),
    },
    controller: {
        // `bdpx.Home-Show` → controller=Home-Show (applies to every controller metric)
        '*': (remainder) => ({ controller: remainder }),
    },
    'third-party': {
        // `bdpx.login.salesforce.com` → host; the host itself contains dots, so we
        // treat the whole remainder as the host for call/latency metrics.
        callsCount: (remainder) => ({ host: remainder }),
        callsP95: (remainder) => ({ host: remainder }),
        // `bdpx.host.socketReadTimeout` → host + exceptionType. The exception type is
        // the final dot-segment; everything before it is the (dotted) host. This is
        // only unambiguous because we key on the remoteExceptions metric.
        remoteExceptions: (remainder) => {
            const lastDot = remainder.lastIndexOf('.');
            if (lastDot > 0) {
                return { host: remainder.slice(0, lastDot), exceptionType: remainder.slice(lastDot + 1) };
            }
            return { host: remainder };
        },
    },
    ecdn: {
        // `2xx bdpx.host` (status class BEFORE the realm) → statusClass + host; other
        // eCDN metrics are just `bdpx.host` → host. Operates on the raw id because
        // the realm is not a leading prefix here.
        successAndError: (_remainder, rawId, realm) => {
            const spaceIdx = rawId.indexOf(' ');
            if (spaceIdx > 0) {
                const statusClass = rawId.slice(0, spaceIdx);
                const host = stripRealmPrefix(rawId.slice(spaceIdx + 1), realm);
                return { statusClass, host };
            }
            return { host: stripRealmPrefix(rawId, realm) };
        },
        '*': (remainder) => ({ host: remainder }),
    },
};
/**
 * Extracts the dimension tags for a single series id.
 *
 * Combines three tiers, most-authoritative last:
 * 1. **Request identity** — `realm`/`environment` from the tenant id (never parsed
 *    from the series string).
 * 2. **String heuristics** — category/metric-specific dimensions parsed from the
 *    packed id (`apiFamily`, `host`, `cacheStatus`, …), or the raw remainder under
 *    `series` when no rule matches.
 * 3. **Applied filters** — any filter that was sent with the request
 *    ({@link MetricsTagContext}) is stamped last, overriding a heuristic guess.
 *    This corrects drill-down ids: with `apiFamily=shopper` the server returns
 *    `bdpx.shopper.auth.v1`, which heuristics would mis-tag as
 *    `apiFamily: "shopper.auth.v1"`; the filter restores `apiFamily: "shopper"`.
 *
 * The result is always a superset of the request context and never throws.
 *
 * @param params - The series' category, metric id, series id, and request context
 * @returns The extracted {@link MetricSeriesTags}
 *
 * @example
 * ```typescript
 * parseSeriesTags({
 *   category: 'scapi', metricId: 'cacheHitRate',
 *   seriesId: 'bdpx.product HIT', context: {tenantId: 'f_ecom_bdpx_prd'},
 * });
 * // → { realm: 'bdpx', environment: 'prd', apiFamily: 'product', cacheStatus: 'HIT' }
 * ```
 */
export function parseSeriesTags(params) {
    const { category, metricId, seriesId, context } = params;
    const { realm, environment } = splitRealmEnvironment(context.tenantId);
    const tags = { realm };
    if (environment)
        tags.environment = environment;
    const categoryRules = EXTRACTORS[category];
    const extractor = categoryRules?.[metricId] ?? categoryRules?.['*'];
    const remainder = stripRealmPrefix(seriesId, realm);
    if (extractor) {
        Object.assign(tags, extractor(remainder, seriesId, realm));
    }
    else if (remainder && remainder !== metricId) {
        // No rule for this category/metric. Preserve the (realm-stripped) remainder
        // so nothing is lost, unless it is just the metric id echoed back (a
        // value-less fallback series).
        tags.series = remainder;
    }
    // Applied filters are authoritative — stamp them last so they override any
    // heuristic guess from a drilled-down id.
    for (const [field, key] of FILTER_TAG_KEYS) {
        const value = context[field];
        if (value)
            tags[key] = value;
    }
    return tags;
}
/**
 * Enriches a metrics response by adding a structured `tags` map to every series,
 * in place-safe fashion (returns a new response; the input is not mutated).
 *
 * This is the batch counterpart to {@link parseSeriesTags}: it walks every
 * metric and series and attaches `series.tags` derived from the series id, the
 * metric id, the category, and the request context. Existing fields (`id`,
 * `name`, `data`) are preserved exactly, so the enriched response is a
 * structural superset — consumers that ignore `tags` are unaffected. The
 * category must be supplied because a {@link MetricsDataResponse} does not carry
 * it (it is implied by the endpoint that produced the response).
 *
 * @param response - The metrics response to enrich
 * @param category - The metric category the response was fetched for
 * @param context - The request identity used to derive `realm`/`environment`
 * @returns A new response with `tags` added to each series
 *
 * @example
 * ```typescript
 * const raw = await getScapiMetrics(client, tenantId);
 * const enriched = enrichMetricsTags(raw, 'scapi', {tenantId});
 * for (const metric of enriched.data) {
 *   for (const series of metric.dataSeries) {
 *     console.log(series.tags, series.data);
 *   }
 * }
 * ```
 */
export function enrichMetricsTags(response, category, context) {
    let unparsed = 0;
    const data = (response.data ?? []).map((metric) => ({
        ...metric,
        dataSeries: (metric.dataSeries ?? []).map((series) => {
            const tags = parseSeriesTags({
                category,
                metricId: metric.metricId,
                seriesId: series.id,
                context,
            });
            // Count series where no dimension beyond identity was recovered, to log
            // once about incomplete coverage without failing.
            const hasDimension = Object.keys(tags).some((k) => k !== 'realm' && k !== 'environment');
            if (!hasDimension)
                unparsed++;
            return { ...series, tags };
        }),
    }));
    if (unparsed > 0) {
        getLogger().debug({ unparsed, category, tenantId: context.tenantId }, 'enrichMetricsTags: some series yielded only identity tags (no category dimensions)');
    }
    return { ...response, data };
}
//# sourceMappingURL=tags.js.map