import { toOrganizationId } from '../../clients/custom-apis.js';
import { getApiErrorMessage } from '../../clients/error-utils.js';
import { getLogger } from '../../logging/logger.js';
import { parseSinceTime, parseRelativeTime } from '../logs/filter.js';
/**
 * All metric categories exposed by the Metrics API, in a stable, documented order.
 *
 * Values match the API path segments (e.g. `overall` → `/metrics/overall`).
 */
export const METRIC_CATEGORIES = [
    'overall',
    'sales',
    'ecdn',
    'third-party',
    'scapi',
    'scapi-hooks',
    'mrt',
    'controller',
    'ocapi',
];
/**
 * How far back the Metrics API retains data: `from` must be no older than
 * `serverNow - 30 days`, or the API returns 400.
 */
export const METRICS_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
/**
 * Default (and maximum) width of a metrics time window: 24 hours.
 *
 * The Metrics API pairs a request that omits `to` with its own `now` and then
 * enforces a maximum window of `to - from ≤ 24h`, rejecting anything wider with a
 * 400. An open-ended `from` older than 24h therefore always fails. To make the
 * behavior predictable, {@link resolveMetricsWindow} fills in a 24-hour window
 * (clamped so `to` never exceeds `now`) for any bound the caller leaves open, and
 * always sends both `from` and `to`. This is only a *default* for derivation — an
 * explicit `from`+`to` range wider than 24h is still sent as-is, letting the API
 * remain authoritative and return its own error.
 */
export const METRICS_DEFAULT_WINDOW_MS = 24 * 60 * 60 * 1000;
/**
 * Safety margin kept *inside* the retention window when clamping `from`. Because
 * the server evaluates retention against its own clock at request time, a `from`
 * computed as exactly "30 days ago" on the client is reliably rejected once
 * request latency and client/server clock skew are added. {@link resolveMetricsWindow}
 * therefore clamps a `from` that lands within this margin of the retention floor
 * up to `now - 30 days + margin`, so edge requests (e.g. `--from 30d`) succeed.
 * 5 minutes comfortably covers latency and typical skew while costing a
 * negligible fraction of the 30-day window.
 */
export const METRICS_RETENTION_SAFETY_MARGIN_MS = 5 * 60 * 1000;
/**
 * Parses a single metrics time bound (`from` or `to`) into a {@link Date}.
 *
 * This is the shared bound parser for the CLI `metrics get` command and the MCP
 * `metrics_get` tool. It resolves a single bound in isolation; deriving the
 * companion bound and applying the 24-hour default window is the job of
 * {@link resolveMetricsWindow}.
 *
 * @param value - The bound: a Date, epoch milliseconds, a relative duration
 *   (`5m`/`1h`/`2d`, relative to `now`), or an ISO 8601 timestamp
 * @param now - Reference time for relative durations (defaults to the current
 *   time; injectable for deterministic tests)
 * @returns The resolved {@link Date}
 * @throws TypeError if a string value is neither a valid relative duration nor
 *   a parseable ISO 8601 timestamp
 *
 * @example
 * ```typescript
 * const from = parseMetricsBound('2d');            // 2 days ago
 * const to = parseMetricsBound('2026-01-25T12:00:00Z');
 * await getSalesMetrics(client, tenantId, {from, to});
 * ```
 */
export function parseMetricsBound(value, now = new Date()) {
    if (value instanceof Date)
        return value;
    if (typeof value === 'number')
        return new Date(value);
    return parseSinceTime(value, now);
}
/**
 * Parses a `--window`/`--for` duration into milliseconds. Accepts a relative
 * string (`1h`, `30m`, `2d`) or a raw number of milliseconds.
 *
 * @throws TypeError if a string is not a valid relative duration
 */
function parseWindowMs(window) {
    if (typeof window === 'number')
        return window;
    const ms = parseRelativeTime(window);
    if (ms === null) {
        throw new TypeError(`Invalid window duration: "${window}". Use a relative duration like "30m", "1h", or "2d".`);
    }
    return ms;
}
/**
 * Resolves `from`/`to`/`window` inputs into the concrete bounds to send to the
 * Metrics API. This is the shared resolver for the CLI `metrics get` command and
 * the MCP `metrics_get` tool, so both share identical, tested semantics.
 *
 * The resolver always produces an explicit `from`+`to` pair. The Metrics API
 * pairs a request that omits `to` with its own `now` and enforces a 24-hour
 * maximum window, so an open-ended `from` older than 24 hours always fails; to
 * make the behavior predictable, any bound the caller leaves open is filled from
 * the 24-hour default window (see {@link METRICS_DEFAULT_WINDOW_MS}) rather than
 * relying on the server's implicit end.
 *
 * The `window` duration makes fixed-width lookbacks easy without hand-computing
 * a second timestamp — e.g. "a 1-hour window starting 7 days ago" is
 * `{from: '7d', window: '1h'}`. Resolution rules:
 *
 * - `from` + `to` — used as given; `window` must NOT also be set. An explicit
 *   range wider than the 24-hour maximum is still sent as-is, leaving the API to
 *   return its own error.
 * - `from` + `window` — `to = from + window`.
 * - `to` + `window` — `from = to - window`.
 * - `window` only — the last `window`: `to = now`, `from = now - window`.
 * - `from` only — a 24-hour window forward from `from`: `to = min(from + 24h, now)`.
 * - `to` only — a 24-hour window back from `to`: `from = to - 24h`.
 * - nothing — the last 24 hours: `to = now`, `from = now - 24h`.
 *
 * Bounds filled from the 24-hour default (the last three rules) set
 * {@link ResolvedMetricsWindow.defaultedWindow}. Validation is structural
 * (over-specification and `from` after `to`). Additionally, a `from` that lands
 * at or beyond the 30-day retention floor is clamped *forward* to
 * `now - 30 days + margin` (see {@link METRICS_RETENTION_SAFETY_MARGIN_MS}) so
 * requests like `--from 30d` are not rejected by the server's own slightly-later
 * clock; the clamp is applied before deriving the companion bound so the window
 * width is preserved, only ever moves `from` toward `now`, and is reported via
 * {@link ResolvedMetricsWindow.clampedFrom}.
 *
 * @param input - The raw `from`/`to`/`window` inputs
 * @param now - Reference time for relative bounds, default/window modes, and the
 *   retention clamp (defaults to the current time; injectable for deterministic tests)
 * @returns The resolved bounds plus ISO/epoch-second echoes and clamp/default flags
 * @throws TypeError if a bound or the window string is unparseable
 * @throws RangeError if all three are supplied, or the resolved `from` is after `to`
 *
 * @example
 * ```typescript
 * // A 1-hour window starting 7 days ago:
 * const w = resolveMetricsWindow({from: '7d', window: '1h'});
 * await getScapiMetrics(client, tenantId, {from: w.from, to: w.to});
 * ```
 */
export function resolveMetricsWindow(input = {}, now = new Date()) {
    const hasFrom = input.from !== undefined && input.from !== '';
    const hasTo = input.to !== undefined && input.to !== '';
    const hasWindow = input.window !== undefined && input.window !== '';
    if (hasFrom && hasTo && hasWindow) {
        throw new RangeError('Specify at most two of from, to, and window — not all three.');
    }
    // Clamp a `from` forward if it predates the retention floor, to survive the
    // server evaluating retention against its own (slightly later) clock. Only
    // ever moves `from` toward `now`, never fetches out-of-range data.
    const earliestSafe = now.getTime() - METRICS_RETENTION_MS + METRICS_RETENTION_SAFETY_MARGIN_MS;
    let clampedFrom = false;
    const clampFrom = (d) => {
        if (d.getTime() < earliestSafe) {
            clampedFrom = true;
            return new Date(earliestSafe);
        }
        return d;
    };
    // Clamp a provided `from` up front so a window derived from it stays inside
    // retention (and a full-width window fits).
    let from = hasFrom ? clampFrom(parseMetricsBound(input.from, now)) : undefined;
    let to = hasTo ? parseMetricsBound(input.to, now) : undefined;
    let defaultedWindow = false;
    if (hasWindow) {
        const windowMs = parseWindowMs(input.window);
        if (from && !to) {
            to = new Date(from.getTime() + windowMs);
        }
        else if (to && !from) {
            from = new Date(to.getTime() - windowMs);
        }
        else {
            // window alone → the last {window}
            to = now;
            from = new Date(now.getTime() - windowMs);
        }
    }
    else if (!(from && to)) {
        // No explicit window and at least one bound open → fill it from the 24-hour
        // default window, sending an explicit range instead of relying on the
        // server's implicit `to = now` + 24h-maximum behavior.
        defaultedWindow = true;
        if (from && !to) {
            // A window forward from `from`, but never past `now` (no future data).
            to = new Date(Math.min(from.getTime() + METRICS_DEFAULT_WINDOW_MS, now.getTime()));
        }
        else if (to && !from) {
            from = new Date(to.getTime() - METRICS_DEFAULT_WINDOW_MS);
        }
        else {
            // Nothing supplied → the last 24 hours.
            to = now;
            from = new Date(now.getTime() - METRICS_DEFAULT_WINDOW_MS);
        }
    }
    // A `from` derived from `to` (via window or the default) may itself predate
    // retention; clamp it forward too. Idempotent for an already-clamped bound.
    from = clampFrom(from);
    if (from.getTime() > to.getTime()) {
        throw new RangeError(`Invalid time window: from (${from.toISOString()}) must be before to (${to.toISOString()}).`);
    }
    return {
        from: from,
        to: to,
        fromIso: from.toISOString(),
        toIso: to.toISOString(),
        fromEpochSeconds: toEpochSeconds(from),
        toEpochSeconds: toEpochSeconds(to),
        clampedFrom,
        defaultedWindow,
    };
}
/**
 * Converts a millisecond time input ({@link Date} or epoch ms) to the epoch
 * **seconds** the Metrics API expects on the wire.
 */
function toEpochSeconds(value) {
    const ms = value instanceof Date ? value.getTime() : value;
    return Math.floor(ms / 1000);
}
/**
 * Normalizes an optional time-window into the query object openapi-fetch expects.
 * Converts millisecond inputs to epoch seconds and drops undefined values so they
 * are not serialized.
 */
function timeWindowQuery(options) {
    const query = {};
    if (options?.from !== undefined)
        query.from = toEpochSeconds(options.from);
    if (options?.to !== undefined)
        query.to = toEpochSeconds(options.to);
    return query;
}
/**
 * Normalizes a successful metrics response for JS consumers: rewrites every data
 * point timestamp from the API's epoch **seconds** to epoch **milliseconds** so
 * that `new Date(point.timestamp)` yields the correct instant. Returns a new
 * response object; the input is not mutated.
 */
function normalizeResponse(data) {
    return {
        ...data,
        data: (data.data ?? []).map((metric) => ({
            ...metric,
            dataSeries: (metric.dataSeries ?? []).map((series) => ({
                ...series,
                data: (series.data ?? []).map((point) => ({ ...point, timestamp: point.timestamp * 1000 })),
            })),
        })),
    };
}
/**
 * Wraps a failed metrics request in a descriptive Error.
 */
function metricsError(category, error, response) {
    return new Error(`Failed to get ${category} metrics: ${getApiErrorMessage(error, response)}`, { cause: error });
}
/**
 * Retrieves overall application metrics for an organization.
 *
 * @param client - Metrics API client from {@link createMetricsClient}
 * @param tenantId - Tenant ID (with or without the `f_ecom_` prefix)
 * @param options - Optional time window (epoch milliseconds)
 * @returns The metrics data response
 * @throws Error if the request fails
 */
export async function getOverallMetrics(client, tenantId, options) {
    getLogger().debug({ tenantId, options }, 'Fetching overall metrics');
    const { data, error, response } = await client.GET('/organizations/{organizationId}/metrics/overall', {
        params: { path: { organizationId: toOrganizationId(tenantId) }, query: timeWindowQuery(options) },
    });
    if (error || !data)
        throw metricsError('overall', error, response);
    return normalizeResponse(data);
}
/**
 * Retrieves sales metrics for an organization.
 *
 * @param client - Metrics API client from {@link createMetricsClient}
 * @param tenantId - Tenant ID (with or without the `f_ecom_` prefix)
 * @param options - Optional time window (epoch milliseconds)
 * @returns The metrics data response
 * @throws Error if the request fails
 */
export async function getSalesMetrics(client, tenantId, options) {
    getLogger().debug({ tenantId, options }, 'Fetching sales metrics');
    const { data, error, response } = await client.GET('/organizations/{organizationId}/metrics/sales', {
        params: { path: { organizationId: toOrganizationId(tenantId) }, query: timeWindowQuery(options) },
    });
    if (error || !data)
        throw metricsError('sales', error, response);
    return normalizeResponse(data);
}
/**
 * Retrieves embedded CDN (eCDN) metrics for an organization.
 *
 * @param client - Metrics API client from {@link createMetricsClient}
 * @param tenantId - Tenant ID (with or without the `f_ecom_` prefix)
 * @param options - Optional time window (epoch milliseconds)
 * @returns The metrics data response
 * @throws Error if the request fails
 */
export async function getEcdnMetrics(client, tenantId, options) {
    getLogger().debug({ tenantId, options }, 'Fetching ecdn metrics');
    const { data, error, response } = await client.GET('/organizations/{organizationId}/metrics/ecdn', {
        params: { path: { organizationId: toOrganizationId(tenantId) }, query: timeWindowQuery(options) },
    });
    if (error || !data)
        throw metricsError('ecdn', error, response);
    return normalizeResponse(data);
}
/**
 * Retrieves third-party service call metrics for an organization.
 *
 * @param client - Metrics API client from {@link createMetricsClient}
 * @param tenantId - Tenant ID (with or without the `f_ecom_` prefix)
 * @param options - Optional time window and `thirdPartyServiceId` filter
 * @returns The metrics data response
 * @throws Error if the request fails
 */
export async function getThirdPartyMetrics(client, tenantId, options) {
    getLogger().debug({ tenantId, options }, 'Fetching third-party metrics');
    const { data, error, response } = await client.GET('/organizations/{organizationId}/metrics/third-party', {
        params: {
            path: { organizationId: toOrganizationId(tenantId) },
            query: { ...timeWindowQuery(options), thirdPartyServiceId: options?.thirdPartyServiceId },
        },
    });
    if (error || !data)
        throw metricsError('third-party', error, response);
    return normalizeResponse(data);
}
/**
 * Retrieves SCAPI request metrics for an organization.
 *
 * @param client - Metrics API client from {@link createMetricsClient}
 * @param tenantId - Tenant ID (with or without the `f_ecom_` prefix)
 * @param options - Optional time window and `apiFamily`/`apiName` filters
 * @returns The metrics data response
 * @throws Error if the request fails
 */
export async function getScapiMetrics(client, tenantId, options) {
    getLogger().debug({ tenantId, options }, 'Fetching scapi metrics');
    const { data, error, response } = await client.GET('/organizations/{organizationId}/metrics/scapi', {
        params: {
            path: { organizationId: toOrganizationId(tenantId) },
            query: { ...timeWindowQuery(options), apiFamily: options?.apiFamily, apiName: options?.apiName },
        },
    });
    if (error || !data)
        throw metricsError('scapi', error, response);
    return normalizeResponse(data);
}
/**
 * Retrieves SCAPI hook execution metrics for an organization.
 *
 * @param client - Metrics API client from {@link createMetricsClient}
 * @param tenantId - Tenant ID (with or without the `f_ecom_` prefix)
 * @param options - Optional time window (epoch milliseconds)
 * @returns The metrics data response
 * @throws Error if the request fails
 */
export async function getScapiHooksMetrics(client, tenantId, options) {
    getLogger().debug({ tenantId, options }, 'Fetching scapi-hooks metrics');
    const { data, error, response } = await client.GET('/organizations/{organizationId}/metrics/scapi-hooks', {
        params: { path: { organizationId: toOrganizationId(tenantId) }, query: timeWindowQuery(options) },
    });
    if (error || !data)
        throw metricsError('scapi-hooks', error, response);
    return normalizeResponse(data);
}
/**
 * Retrieves Managed Runtime (MRT) metrics for an organization.
 *
 * @param client - Metrics API client from {@link createMetricsClient}
 * @param tenantId - Tenant ID (with or without the `f_ecom_` prefix)
 * @param options - Optional time window (epoch milliseconds)
 * @returns The metrics data response
 * @throws Error if the request fails
 */
export async function getMrtMetrics(client, tenantId, options) {
    getLogger().debug({ tenantId, options }, 'Fetching mrt metrics');
    const { data, error, response } = await client.GET('/organizations/{organizationId}/metrics/mrt', {
        params: { path: { organizationId: toOrganizationId(tenantId) }, query: timeWindowQuery(options) },
    });
    if (error || !data)
        throw metricsError('mrt', error, response);
    return normalizeResponse(data);
}
/**
 * Retrieves controller/pipeline metrics for an organization.
 *
 * @param client - Metrics API client from {@link createMetricsClient}
 * @param tenantId - Tenant ID (with or without the `f_ecom_` prefix)
 * @param options - Optional time window (epoch milliseconds)
 * @returns The metrics data response
 * @throws Error if the request fails
 */
export async function getControllerMetrics(client, tenantId, options) {
    getLogger().debug({ tenantId, options }, 'Fetching controller metrics');
    const { data, error, response } = await client.GET('/organizations/{organizationId}/metrics/controller', {
        params: { path: { organizationId: toOrganizationId(tenantId) }, query: timeWindowQuery(options) },
    });
    if (error || !data)
        throw metricsError('controller', error, response);
    return normalizeResponse(data);
}
/**
 * Retrieves OCAPI request metrics for an organization.
 *
 * @param client - Metrics API client from {@link createMetricsClient}
 * @param tenantId - Tenant ID (with or without the `f_ecom_` prefix)
 * @param options - Optional time window and `ocapiCategory`/`ocapiApi` filters
 * @returns The metrics data response
 * @throws Error if the request fails
 */
export async function getOcapiMetrics(client, tenantId, options) {
    getLogger().debug({ tenantId, options }, 'Fetching ocapi metrics');
    const { data, error, response } = await client.GET('/organizations/{organizationId}/metrics/ocapi', {
        params: {
            path: { organizationId: toOrganizationId(tenantId) },
            query: { ...timeWindowQuery(options), ocapiCategory: options?.ocapiCategory, ocapiApi: options?.ocapiApi },
        },
    });
    if (error || !data)
        throw metricsError('ocapi', error, response);
    return normalizeResponse(data);
}
/**
 * Retrieves metrics for a category by name, dispatching to the category-specific
 * function. Category-specific filters (`thirdPartyServiceId`, `apiFamily`,
 * `apiName`, `ocapiCategory`, `ocapiApi`) are applied only for the categories
 * that support them and ignored otherwise.
 *
 * @param client - Metrics API client from {@link createMetricsClient}
 * @param tenantId - Tenant ID (with or without the `f_ecom_` prefix)
 * @param category - One of {@link METRIC_CATEGORIES}
 * @param options - Optional time window and category-specific filters
 * @returns The metrics data response
 * @throws Error if the request fails or the category is unknown
 */
export async function getMetricsByCategory(client, tenantId, category, options) {
    switch (category) {
        case 'overall':
            return getOverallMetrics(client, tenantId, options);
        case 'sales':
            return getSalesMetrics(client, tenantId, options);
        case 'ecdn':
            return getEcdnMetrics(client, tenantId, options);
        case 'third-party':
            return getThirdPartyMetrics(client, tenantId, options);
        case 'scapi':
            return getScapiMetrics(client, tenantId, options);
        case 'scapi-hooks':
            return getScapiHooksMetrics(client, tenantId, options);
        case 'mrt':
            return getMrtMetrics(client, tenantId, options);
        case 'controller':
            return getControllerMetrics(client, tenantId, options);
        case 'ocapi':
            return getOcapiMetrics(client, tenantId, options);
        default: {
            // Exhaustiveness guard: if a new category is added to METRIC_CATEGORIES
            // without a case here, this line fails to compile.
            const exhaustive = category;
            throw new Error(`Unknown metric category: ${String(exhaustive)}`);
        }
    }
}
// Client-side dimension/tag extraction for series (interim until the API
// exposes structured tags server-side). See ./tags.ts.
export { parseSeriesTags, enrichMetricsTags } from './tags.js';
//# sourceMappingURL=index.js.map